text-gen 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/text/gen/context.rb +119 -0
- data/lib/text/gen/filter/base.rb +16 -13
- data/lib/text/gen/filter/capitalize.rb +19 -0
- data/lib/text/gen/filter/censor.rb +52 -0
- data/lib/text/gen/filter/clear.rb +19 -0
- data/lib/text/gen/filter/distinct.rb +19 -0
- data/lib/text/gen/filter/downcase.rb +19 -0
- data/lib/text/gen/filter/exclude.rb +19 -0
- data/lib/text/gen/filter/locale.rb +44 -0
- data/lib/text/gen/filter/match.rb +19 -0
- data/lib/text/gen/filter/meta.rb +35 -0
- data/lib/text/gen/filter/pluralize.rb +96 -0
- data/lib/text/gen/filter/reject.rb +24 -0
- data/lib/text/gen/filter/remember.rb +18 -0
- data/lib/text/gen/filter/replace.rb +15 -0
- data/lib/text/gen/filter/select.rb +24 -0
- data/lib/text/gen/filter/swap.rb +50 -0
- data/lib/text/gen/filter/titleize.rb +29 -0
- data/lib/text/gen/filter/upcase.rb +19 -0
- data/lib/text/gen/filter.rb +10 -168
- data/lib/text/gen/investigator.rb +38 -0
- data/lib/text/gen/meta.rb +46 -0
- data/lib/text/gen/result.rb +7 -22
- data/lib/text/gen/result_accumulator.rb +6 -11
- data/lib/text/gen/runner.rb +113 -174
- data/lib/text/gen/store.rb +29 -1
- data/lib/text/gen/version.rb +1 -1
- data/lib/text/gen.rb +4 -0
- metadata +21 -17
- data/lib/text/gen/filter/item/locale.rb +0 -29
- data/lib/text/gen/filter/item/reject.rb +0 -28
- data/lib/text/gen/filter/item/select.rb +0 -28
- data/lib/text/gen/filter/item_filter.rb +0 -17
- data/lib/text/gen/filter/result/capitalize.rb +0 -17
- data/lib/text/gen/filter/result/censor.rb +0 -55
- data/lib/text/gen/filter/result/clear.rb +0 -19
- data/lib/text/gen/filter/result/downcase.rb +0 -17
- data/lib/text/gen/filter/result/exclude.rb +0 -22
- data/lib/text/gen/filter/result/match.rb +0 -22
- data/lib/text/gen/filter/result/meta.rb +0 -31
- data/lib/text/gen/filter/result/pluralize.rb +0 -91
- data/lib/text/gen/filter/result/swap.rb +0 -48
- data/lib/text/gen/filter/result/titleize.rb +0 -28
- data/lib/text/gen/filter/result/upcase.rb +0 -17
- data/lib/text/gen/filter/result_filter.rb +0 -35
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Text
|
|
4
|
+
module Gen
|
|
5
|
+
module Filter
|
|
6
|
+
class Titleize < Base
|
|
7
|
+
SKIP_WORDS = Set.new(%w[a an and as at but by for if in of on or the to v via vs])
|
|
8
|
+
|
|
9
|
+
def result(context, result)
|
|
10
|
+
return result if @depth && context.depth != @depth
|
|
11
|
+
|
|
12
|
+
text = result.text
|
|
13
|
+
text = titleize(text)
|
|
14
|
+
return result if text == result.text
|
|
15
|
+
|
|
16
|
+
Result.from(result, text:, type: component_key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def titleize(str)
|
|
22
|
+
str.split(/\s+/).map.with_index do |word, idx|
|
|
23
|
+
idx.zero? || !SKIP_WORDS.include?(word) ? word.sub(/\S/, &:upcase) : word
|
|
24
|
+
end.join(" ")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Text
|
|
4
|
+
module Gen
|
|
5
|
+
module Filter
|
|
6
|
+
class Upcase < Base
|
|
7
|
+
def result(context, result)
|
|
8
|
+
return result if @depth && context.depth != @depth
|
|
9
|
+
|
|
10
|
+
text = result.text
|
|
11
|
+
text = text.upcase
|
|
12
|
+
return result if text == result.text
|
|
13
|
+
|
|
14
|
+
Result.from(result, text:, type: component_key)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/text/gen/filter.rb
CHANGED
|
@@ -1,183 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "filter/base"
|
|
4
|
-
|
|
5
|
-
require_relative "filter/item_filter"
|
|
6
|
-
|
|
7
|
-
# Result filters
|
|
8
|
-
require_relative "filter/result/capitalize"
|
|
9
|
-
require_relative "filter/result/censor"
|
|
10
|
-
require_relative "filter/result/clear"
|
|
11
|
-
require_relative "filter/result/downcase"
|
|
12
|
-
require_relative "filter/result/exclude"
|
|
13
|
-
require_relative "filter/result/match"
|
|
14
|
-
require_relative "filter/result/meta"
|
|
15
|
-
require_relative "filter/result/pluralize"
|
|
16
|
-
require_relative "filter/result/swap"
|
|
17
|
-
require_relative "filter/result/titleize"
|
|
18
|
-
require_relative "filter/result/upcase"
|
|
19
|
-
|
|
20
|
-
# Item filters
|
|
21
|
-
require_relative "filter/item/locale"
|
|
22
|
-
require_relative "filter/item/select"
|
|
23
|
-
require_relative "filter/item/reject"
|
|
4
|
+
Dir[File.join(__dir__, "filter", "*.rb")].each { |f| require f unless f.end_with?("/base.rb") }
|
|
24
5
|
|
|
25
6
|
module Text
|
|
26
7
|
module Gen
|
|
27
8
|
module Filter
|
|
28
|
-
RESULT_FILTER_CLASSES = {
|
|
29
|
-
"capitalize" => Filter::Result::Capitalize,
|
|
30
|
-
"censor" => Filter::Result::Censor,
|
|
31
|
-
"clear" => Filter::Result::Clear,
|
|
32
|
-
"downcase" => Filter::Result::Downcase,
|
|
33
|
-
"exclude" => Filter::Result::Exclude,
|
|
34
|
-
"match" => Filter::Result::Match,
|
|
35
|
-
"meta" => Filter::Result::Meta,
|
|
36
|
-
"pluralize" => Filter::Result::Pluralize,
|
|
37
|
-
"swap" => Filter::Result::Swap,
|
|
38
|
-
"titleize" => Filter::Result::Titleize,
|
|
39
|
-
"upcase" => Filter::Result::Upcase
|
|
40
|
-
}.freeze
|
|
41
|
-
|
|
42
|
-
ITEM_FILTER_CLASSES = {
|
|
43
|
-
"locale" => Filter::Item::Locale,
|
|
44
|
-
"select" => Filter::Item::Select,
|
|
45
|
-
"reject" => Filter::Item::Reject
|
|
46
|
-
}.freeze
|
|
47
|
-
|
|
48
9
|
class << self
|
|
49
|
-
def functions(result, filters, lookup: nil)
|
|
50
|
-
return result if filters.nil? || filters.empty?
|
|
51
|
-
|
|
52
|
-
filters.each do |filter|
|
|
53
|
-
filter_class = RESULT_FILTER_CLASSES[filter["type"]]
|
|
54
|
-
if filter_class
|
|
55
|
-
filter_instance = filter_class.new(filter, lookup: lookup)
|
|
56
|
-
result = filter_instance.apply(result)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
result
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def filter_locale(items, filters)
|
|
64
|
-
locale_filters = filters_by_type(filters, "locale")
|
|
65
|
-
return items if locale_filters.empty?
|
|
66
|
-
|
|
67
|
-
filter_instance = ITEM_FILTER_CLASSES["locale"].new(locale_filters.first)
|
|
68
|
-
filter_instance.apply(items)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def filter_select(items, filters)
|
|
72
|
-
select_filters = filters_by_type(filters, "select")
|
|
73
|
-
return items if select_filters.empty?
|
|
74
|
-
|
|
75
|
-
# OR logic: any filter matches
|
|
76
|
-
items.select do |item|
|
|
77
|
-
select_filters.any? do |filter|
|
|
78
|
-
filter_instance = ITEM_FILTER_CLASSES["select"].new(filter)
|
|
79
|
-
filter_instance.apply([item]).any?
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def filter_reject(items, filters)
|
|
85
|
-
reject_filters = filters_by_type(filters, "reject")
|
|
86
|
-
return items if reject_filters.empty?
|
|
87
|
-
|
|
88
|
-
# AND logic: all filters must pass
|
|
89
|
-
reject_filters.each do |filter|
|
|
90
|
-
filter_instance = ITEM_FILTER_CLASSES["reject"].new(filter)
|
|
91
|
-
items = filter_instance.apply(items)
|
|
92
|
-
end
|
|
93
|
-
items
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def result_select(result, filters)
|
|
97
|
-
match_filters = filters_by_type(filters, "match")
|
|
98
|
-
return result if match_filters.empty?
|
|
99
|
-
|
|
100
|
-
# OR logic: any filter matches
|
|
101
|
-
match_filters.any? do |filter|
|
|
102
|
-
filter_instance = RESULT_FILTER_CLASSES["match"].new(filter)
|
|
103
|
-
filter_instance.apply(result)
|
|
104
|
-
end ? result : nil
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def result_reject(result, filters)
|
|
108
|
-
exclude_filters = filters_by_type(filters, "exclude")
|
|
109
|
-
return result if exclude_filters.empty?
|
|
110
|
-
|
|
111
|
-
# AND logic: all filters must pass
|
|
112
|
-
exclude_filters.all? do |filter|
|
|
113
|
-
filter_instance = RESULT_FILTER_CLASSES["exclude"].new(filter)
|
|
114
|
-
filter_instance.apply(result)
|
|
115
|
-
end ? result : nil
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Create a builder that always returns a constant value
|
|
119
|
-
def constant_builder(key, str)
|
|
120
|
-
{
|
|
121
|
-
"key" => key,
|
|
122
|
-
"items" => [constant_item(str)],
|
|
123
|
-
"meta" => {},
|
|
124
|
-
"filters" => []
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def constant_item(str, item = nil)
|
|
129
|
-
segments = Segment::Parser.parse(str)
|
|
130
|
-
citem = { "segments" => segments }
|
|
131
|
-
if item
|
|
132
|
-
citem["value"] = item["value"] if item["value"]
|
|
133
|
-
citem["multiplier"] = item["multiplier"] if item["multiplier"]
|
|
134
|
-
citem["filters"] = item["filters"] if item["filters"]
|
|
135
|
-
citem["meta"] = item["meta"] if item["meta"]
|
|
136
|
-
end
|
|
137
|
-
citem
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def filter_by_type(filters, type)
|
|
141
|
-
filters&.find { |f| f["type"] == type }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def filters_by_type(filters, type)
|
|
145
|
-
return [] if filters.nil? || filters.empty?
|
|
146
|
-
|
|
147
|
-
filters.select { |f| f["type"] == type }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Returns a hash of all available filters with their parameters
|
|
151
|
-
# Filters that require explicit parameters will have an array of parameter names
|
|
152
|
-
# Filters that don't require parameters will have an empty array
|
|
153
10
|
def available_filters
|
|
154
|
-
|
|
155
|
-
hash[name] = filter_parameters(name)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
item_filters = ITEM_FILTER_CLASSES.keys.each_with_object({}) do |name, hash|
|
|
159
|
-
hash[name] = filter_parameters(name)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
result_filters.merge(item_filters)
|
|
11
|
+
Base.subclasses
|
|
163
12
|
end
|
|
164
13
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"swap" => ["key", "value"],
|
|
171
|
-
"meta" => ["key", "value"],
|
|
172
|
-
"censor" => ["key", "value"],
|
|
173
|
-
"match" => ["key", "value"],
|
|
174
|
-
"exclude" => ["key", "value"],
|
|
175
|
-
"select" => ["key", "value"],
|
|
176
|
-
"reject" => ["key", "value"]
|
|
177
|
-
}.freeze
|
|
14
|
+
def build(hsh_or_key, depth)
|
|
15
|
+
hsh = hsh_or_key.is_a?(Hash) ? hsh_or_key : key_to_hsh(hsh_or_key)
|
|
16
|
+
filter_class = available_filters.find { |f| f.filter_name == hsh["type"] }
|
|
17
|
+
filter_class.new(hsh, depth) if filter_class
|
|
18
|
+
end
|
|
178
19
|
|
|
179
|
-
def
|
|
180
|
-
|
|
20
|
+
def key_to_hsh(key)
|
|
21
|
+
type, key, value = key.split(":", 3)
|
|
22
|
+
{ "type" => type, "text" => key, "value" => value }
|
|
181
23
|
end
|
|
182
24
|
end
|
|
183
25
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Text
|
|
4
|
+
module Gen
|
|
5
|
+
class Investigator
|
|
6
|
+
attr_reader :key, :lookup, :max_recursion
|
|
7
|
+
|
|
8
|
+
def initialize(key:, lookup:, max_recursion: 10)
|
|
9
|
+
@store = Store.new(lookup)
|
|
10
|
+
@key = key.to_s.downcase
|
|
11
|
+
@max_recursion = max_recursion
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def investigate
|
|
15
|
+
visit_and_recurse(key, 0).sort.uniq
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_and_recurse(key, depth)
|
|
19
|
+
depth += 1
|
|
20
|
+
raise MaxRecursionError if depth > max_recursion
|
|
21
|
+
|
|
22
|
+
builder = store.fetch(key)
|
|
23
|
+
keys = [key]
|
|
24
|
+
builder["items"].each do |item|
|
|
25
|
+
item["segments"].each do |segment|
|
|
26
|
+
if segment["type"] == "reference"
|
|
27
|
+
keys << segment["text"]
|
|
28
|
+
keys += visit_and_recurse(segment["text"], depth)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
keys
|
|
33
|
+
rescue StandardError
|
|
34
|
+
[key]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Text
|
|
4
|
+
module Gen
|
|
5
|
+
class Meta
|
|
6
|
+
class << self
|
|
7
|
+
def merge_meta(lhs, rhs)
|
|
8
|
+
return {} unless lhs && rhs
|
|
9
|
+
return lhs.dup if rhs.empty?
|
|
10
|
+
return rhs.dup if lhs.empty?
|
|
11
|
+
|
|
12
|
+
hsh = {}
|
|
13
|
+
lhs.each { |k, v| append_kv(hsh, k, v) }
|
|
14
|
+
rhs.each { |k, v| append_kv(hsh, k, v) }
|
|
15
|
+
hsh
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def append_kv(hsh, key, val)
|
|
19
|
+
key = key.to_s.downcase
|
|
20
|
+
arr = val.is_a?(Array) ? val : Array(val)
|
|
21
|
+
hsh[key] = hsh.key?(key) ? (hsh[key] + arr).uniq : arr.dup
|
|
22
|
+
hsh
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clear_kv(hsh, key, val)
|
|
26
|
+
return {} if key == "*" && val == "*"
|
|
27
|
+
|
|
28
|
+
new_hsh = {}
|
|
29
|
+
if key == "*"
|
|
30
|
+
hsh.each_key { |k| new_hsh[k] = hsh[k].dup.reject { |v| v == val } }
|
|
31
|
+
elsif val == "*"
|
|
32
|
+
hsh.each_key { |k| new_hsh[k] = hsh[k].dup unless k == key }
|
|
33
|
+
else
|
|
34
|
+
hsh.each_key do |k|
|
|
35
|
+
if hsh.key?(k)
|
|
36
|
+
new_hsh[k] = k == key ? hsh[k].select { |v| v != val } : hsh[k].dup
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
new_hsh
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/text/gen/result.rb
CHANGED
|
@@ -30,35 +30,19 @@ module Text
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def merge_all_meta(results)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
results.each do |r|
|
|
36
|
-
r.meta.each { |k, v| merge_kv(k, v) }
|
|
37
|
-
end
|
|
33
|
+
(results || []).each { |r| @meta = Text::Gen::Meta.merge_meta(meta, r.meta) }
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def merge_meta(hsh)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
hsh.each { |k, v| merge_kv(k, v) }
|
|
37
|
+
@meta = Text::Gen::Meta.merge_meta(meta, hsh)
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def merge_kv(key, val)
|
|
47
|
-
key
|
|
48
|
-
arr = val.is_a?(Array) ? val : Array(val)
|
|
49
|
-
meta[key] = meta.key?(key) ? (meta[key] + arr).uniq : arr.dup
|
|
41
|
+
Text::Gen::Meta.append_kv(meta, key, val)
|
|
50
42
|
end
|
|
51
43
|
|
|
52
44
|
def clear_meta(key, val)
|
|
53
|
-
|
|
54
|
-
@meta = {}
|
|
55
|
-
elsif key == "*"
|
|
56
|
-
@meta.each_value { |v| v.delete(val) }
|
|
57
|
-
elsif val == "*"
|
|
58
|
-
@meta.delete(key)
|
|
59
|
-
else
|
|
60
|
-
@meta[key]&.delete(val)
|
|
61
|
-
end
|
|
45
|
+
@meta = Text::Gen::Meta.clear_kv(meta, key, val)
|
|
62
46
|
end
|
|
63
47
|
|
|
64
48
|
def to_s
|
|
@@ -66,12 +50,13 @@ module Text
|
|
|
66
50
|
end
|
|
67
51
|
|
|
68
52
|
class << self
|
|
69
|
-
def from(
|
|
53
|
+
def from(result, type:, text: nil, value: nil, multiplier: nil, meta: nil)
|
|
54
|
+
text ||= result.text
|
|
70
55
|
value ||= result.value
|
|
71
56
|
multiplier ||= result.multiplier
|
|
72
57
|
new(text:, type:, value:, multiplier:).tap do |s|
|
|
73
58
|
s.components.append(result)
|
|
74
|
-
s.merge_meta(result.meta)
|
|
59
|
+
s.merge_meta(meta || result.meta)
|
|
75
60
|
end
|
|
76
61
|
end
|
|
77
62
|
|
|
@@ -5,16 +5,15 @@ module Text
|
|
|
5
5
|
# ResultAccumulator handles accumulating results with attempt tracking
|
|
6
6
|
class ResultAccumulator
|
|
7
7
|
class << self
|
|
8
|
-
def accumulate(
|
|
9
|
-
new(
|
|
8
|
+
def accumulate(count:, max_attempts:, &)
|
|
9
|
+
new(count: count, max_attempts: max_attempts).accumulate(&)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def initialize(
|
|
14
|
-
@unique = unique
|
|
13
|
+
def initialize(count:, max_attempts:)
|
|
15
14
|
@count = [count, 1].max
|
|
16
15
|
@max_attempts = [max_attempts, 1].max
|
|
17
|
-
@results =
|
|
16
|
+
@results = []
|
|
18
17
|
@attempts = max_attempts * count
|
|
19
18
|
end
|
|
20
19
|
|
|
@@ -27,17 +26,13 @@ module Text
|
|
|
27
26
|
raise MaxAttemptsError if @attempts.zero?
|
|
28
27
|
end
|
|
29
28
|
|
|
30
|
-
@results
|
|
29
|
+
@results
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
private
|
|
34
33
|
|
|
35
34
|
def add_result(result)
|
|
36
|
-
|
|
37
|
-
@results[result.text] = result
|
|
38
|
-
else
|
|
39
|
-
@results << [result.text, result]
|
|
40
|
-
end
|
|
35
|
+
@results << result
|
|
41
36
|
end
|
|
42
37
|
end
|
|
43
38
|
end
|