text-gen 0.12.5 → 0.13.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/CHANGELOG.md +4 -1
- data/lib/text/gen/context.rb +14 -3
- data/lib/text/gen/filter/capitalize.rb +2 -0
- data/lib/text/gen/filter/censor.rb +3 -0
- data/lib/text/gen/filter/clear.rb +1 -1
- data/lib/text/gen/filter/distinct.rb +1 -1
- data/lib/text/gen/filter/downcase.rb +1 -0
- data/lib/text/gen/filter/exclude.rb +6 -4
- data/lib/text/gen/filter/match.rb +2 -0
- data/lib/text/gen/filter/meta.rb +1 -0
- data/lib/text/gen/filter/pluralize.rb +66 -13
- data/lib/text/gen/filter/reject.rb +2 -0
- data/lib/text/gen/filter/select.rb +4 -1
- data/lib/text/gen/runner.rb +35 -4
- data/lib/text/gen/store.rb +21 -2
- data/lib/text/gen/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2afbd1cd20f4be4fa1c6ebb125fda4894f46026a078b8b54f808ed1e9fb684e6
|
|
4
|
+
data.tar.gz: '043274280f1ff52837206196ee489932b64c85f1a2af037b9b29cf1127ae1236'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cce97e373664b52db5752f18d0a672041246486fdd4848aeb74febf96f099543ac04ac97c5609ea71877ac867afa9a805a51862d5099ddad3dc74be81f9579ea
|
|
7
|
+
data.tar.gz: 5762c3a92aca626f66e5127ab9aef91bc7a14bce1111e44836f7b083b7cc35ee387b4f8f5f0ea4351d38fad789a3bdde015dfce7f81257f78740f5627870f8c6
|
data/CHANGELOG.md
CHANGED
data/lib/text/gen/context.rb
CHANGED
|
@@ -4,6 +4,8 @@ module Text
|
|
|
4
4
|
module Gen
|
|
5
5
|
# Keep the context for one run.
|
|
6
6
|
class Context
|
|
7
|
+
KNOWN_STRATEGIES = Set.new(%w[random weighted sequence sample])
|
|
8
|
+
|
|
7
9
|
attr_reader :store, :depth
|
|
8
10
|
|
|
9
11
|
def initialize(store:, filters: nil, meta: nil, max_recursion: 10)
|
|
@@ -22,10 +24,19 @@ module Text
|
|
|
22
24
|
def descend!(builder)
|
|
23
25
|
@depth += 1
|
|
24
26
|
@keys << builder["key"]
|
|
25
|
-
@strategy_stack << (builder
|
|
27
|
+
@strategy_stack << strategy_to_stack_item(builder)
|
|
26
28
|
raise MaxRecursionError if @depth > @max_recursion
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
def strategy_to_stack_item(builder)
|
|
32
|
+
strategy, modifier = builder.fetch("strategy", "weighted").split(":", 2)
|
|
33
|
+
unless KNOWN_STRATEGIES.include?(strategy)
|
|
34
|
+
modifier = strategy
|
|
35
|
+
strategy = "weighted"
|
|
36
|
+
end
|
|
37
|
+
[strategy, modifier]
|
|
38
|
+
end
|
|
39
|
+
|
|
29
40
|
def ascend!
|
|
30
41
|
@depth -= 1
|
|
31
42
|
@keys.pop
|
|
@@ -50,7 +61,7 @@ module Text
|
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
def current_strategy
|
|
53
|
-
@strategy_stack.last&.first || "
|
|
64
|
+
@strategy_stack.last&.first || "weighted"
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
def current_modifier
|
|
@@ -64,7 +75,7 @@ module Text
|
|
|
64
75
|
|
|
65
76
|
def remembered(key = nil)
|
|
66
77
|
key ||= current_key
|
|
67
|
-
@remembered.select {|k, _r| k == key }.map(&:last)
|
|
78
|
+
@remembered.select { |k, _r| k == key }.map(&:last)
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
def remember_checkpoint
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Text
|
|
4
4
|
module Gen
|
|
5
5
|
module Filter
|
|
6
|
+
# Capitalize the result text; first character only by default.
|
|
7
|
+
# Use `capitalize:force` to use the default ruby `capitalize` method
|
|
6
8
|
class Capitalize < Base
|
|
7
9
|
def result(context, result)
|
|
8
10
|
return result if @depth && context.depth != @depth
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module Text
|
|
4
4
|
module Gen
|
|
5
5
|
module Filter
|
|
6
|
+
# Censor looks up a different builder and rejects the result if
|
|
7
|
+
# the result text is included in the
|
|
6
8
|
class Censor < Base
|
|
7
9
|
def result(context, result)
|
|
8
10
|
return result if @depth && context.depth != @depth
|
|
11
|
+
|
|
9
12
|
return result if key.nil?
|
|
10
13
|
|
|
11
14
|
builder = context.store.fetch(key)
|
|
@@ -9,7 +9,7 @@ module Text
|
|
|
9
9
|
return result if key != context.current_key
|
|
10
10
|
|
|
11
11
|
previous = context.remembered(key)
|
|
12
|
-
return
|
|
12
|
+
return if previous.any? { |r| r.text.strip.downcase == result.text.strip.downcase }
|
|
13
13
|
|
|
14
14
|
result
|
|
15
15
|
end
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
module Text
|
|
4
4
|
module Gen
|
|
5
5
|
module Filter
|
|
6
|
+
# Exclude is the opposite of `match` and returns nil if
|
|
7
|
+
# the result metadata is matches the pattern in this filter.
|
|
6
8
|
class Exclude < Base
|
|
7
9
|
def result(context, result)
|
|
8
10
|
return result if @depth && context.depth != @depth
|
|
9
|
-
return
|
|
10
|
-
return
|
|
11
|
-
return
|
|
12
|
-
return
|
|
11
|
+
return if result.nil?
|
|
12
|
+
return if value == "*" && result.meta.key?(key)
|
|
13
|
+
return if key == "*" && result.meta.values.any? { |arr| arr.include?(value) }
|
|
14
|
+
return if result.meta[key]&.include?(value)
|
|
13
15
|
|
|
14
16
|
result
|
|
15
17
|
end
|
data/lib/text/gen/filter/meta.rb
CHANGED
|
@@ -30,7 +30,13 @@ module Text
|
|
|
30
30
|
return result if @depth && context.depth != @depth
|
|
31
31
|
|
|
32
32
|
text = result.text
|
|
33
|
-
text =
|
|
33
|
+
text = if key && value
|
|
34
|
+
substitute(text, result.multiplier)
|
|
35
|
+
elsif key
|
|
36
|
+
pluralize_by_key(text, result.multiplier, result)
|
|
37
|
+
else
|
|
38
|
+
pluralize(text, result.multiplier)
|
|
39
|
+
end
|
|
34
40
|
return result if text == result.text
|
|
35
41
|
|
|
36
42
|
Result.from(result, text:, type: component_key)
|
|
@@ -38,27 +44,74 @@ module Text
|
|
|
38
44
|
|
|
39
45
|
private
|
|
40
46
|
|
|
47
|
+
# Splits a string into alternating word and non-word tokens, preserving all characters.
|
|
48
|
+
# e.g. "5 swords." => ["5", " ", "swords", "."]
|
|
49
|
+
def tokenize(str)
|
|
50
|
+
str.scan(/\w+|\W+/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pluralize_by_key(str, multiplier, result)
|
|
54
|
+
tokens = tokenize(str)
|
|
55
|
+
word_tokens = tokens.select { |t| t.match?(/\w/) }
|
|
56
|
+
idx = word_tokens.rindex { |s| to_num(s) }
|
|
57
|
+
num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
|
|
58
|
+
return str if num.nil? || num <= 1
|
|
59
|
+
|
|
60
|
+
component = find_component(result, key)
|
|
61
|
+
return str unless component
|
|
62
|
+
|
|
63
|
+
component_text = component.text
|
|
64
|
+
return str if component_text.nil? || component_text.strip.empty?
|
|
65
|
+
|
|
66
|
+
component_tokens = tokenize(component_text)
|
|
67
|
+
last_word_idx = component_tokens.rindex { |t| t.match?(/\w/) }
|
|
68
|
+
return str unless last_word_idx
|
|
69
|
+
|
|
70
|
+
dc = component_tokens[last_word_idx].downcase
|
|
71
|
+
component_tokens[last_word_idx] = exceptions(dc) || single_letter(dc) || others(dc) || ends_with_y(dc) || simple(dc)
|
|
72
|
+
str.sub(component_text, component_tokens.join)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find_component(result, search_key)
|
|
76
|
+
return result if result.type == search_key
|
|
77
|
+
|
|
78
|
+
result.components.each do |c|
|
|
79
|
+
found = find_component(c, search_key)
|
|
80
|
+
return found if found
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def substitute(str, multiplier = 1)
|
|
86
|
+
tokens = tokenize(str)
|
|
87
|
+
word_tokens = tokens.select { |t| t.match?(/\w/) }
|
|
88
|
+
idx = word_tokens.rindex { |s| to_num(s) }
|
|
89
|
+
num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
|
|
90
|
+
return str if num.nil? || num <= 1
|
|
91
|
+
|
|
92
|
+
tokens.map! { |t| t.match?(/\A#{Regexp.escape(key)}\z/i) ? value : t }
|
|
93
|
+
tokens.join
|
|
94
|
+
end
|
|
95
|
+
|
|
41
96
|
def pluralize(str, multiplier = 1)
|
|
42
97
|
return str if str.empty?
|
|
43
98
|
|
|
44
|
-
|
|
45
|
-
|
|
99
|
+
tokens = tokenize(str)
|
|
100
|
+
word_tokens = tokens.select { |t| t.match?(/\w/) }
|
|
101
|
+
return str if word_tokens.length < 2
|
|
46
102
|
|
|
47
|
-
idx =
|
|
103
|
+
idx = word_tokens.rindex { |s| to_num(s) }
|
|
48
104
|
|
|
49
105
|
# Use the multiplier if available, otherwise parse from text
|
|
50
|
-
num =
|
|
51
|
-
multiplier
|
|
52
|
-
else
|
|
53
|
-
(idx ? to_num(arr[idx]) : nil)
|
|
54
|
-
end
|
|
106
|
+
num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
|
|
55
107
|
|
|
56
108
|
return str if num.nil? || num <= 1
|
|
57
|
-
return str if idx && idx >=
|
|
109
|
+
return str if idx && idx >= word_tokens.length - 1
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
last_word_idx = tokens.rindex { |t| t.match?(/\w/) }
|
|
112
|
+
dc = tokens[last_word_idx].downcase
|
|
113
|
+
tokens[last_word_idx] = exceptions(dc) || single_letter(dc) || others(dc) || ends_with_y(dc) || simple(dc)
|
|
114
|
+
tokens.join
|
|
62
115
|
end
|
|
63
116
|
|
|
64
117
|
def to_num(str)
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module Text
|
|
4
4
|
module Gen
|
|
5
5
|
module Filter
|
|
6
|
+
# Select reduces the items in the array to **only** those with passing
|
|
7
|
+
# metadata. This happens before generation as opposed to `match` which
|
|
8
|
+
# applies a pass/fail to the result.
|
|
6
9
|
class Select < Base
|
|
7
|
-
def
|
|
10
|
+
def items(_context, items)
|
|
8
11
|
items.select do |item|
|
|
9
12
|
pass_select?(item["meta"])
|
|
10
13
|
end
|
data/lib/text/gen/runner.rb
CHANGED
|
@@ -68,12 +68,12 @@ module Text
|
|
|
68
68
|
return if items.empty?
|
|
69
69
|
|
|
70
70
|
case context.current_strategy
|
|
71
|
+
when "sample"
|
|
72
|
+
run_random_item(context, items)
|
|
71
73
|
when "sequence"
|
|
72
74
|
run_item_sequence(context, items)
|
|
73
|
-
when "weighted"
|
|
74
|
-
run_weighted_items(context, items)
|
|
75
75
|
else
|
|
76
|
-
|
|
76
|
+
run_weighted_items(context, items)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -94,7 +94,7 @@ module Text
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def run_weighted_items(context, items)
|
|
97
|
-
total_weight =
|
|
97
|
+
total_weight = calculate_total_weight(context, items)
|
|
98
98
|
dice = context.current_modifier
|
|
99
99
|
rand_weight = if dice.nil? || dice.empty? || dice == "*"
|
|
100
100
|
rand(total_weight)
|
|
@@ -104,6 +104,7 @@ module Text
|
|
|
104
104
|
end
|
|
105
105
|
return if rand_weight > total_weight
|
|
106
106
|
|
|
107
|
+
|
|
107
108
|
current_weight = 0
|
|
108
109
|
item = items.find do |item|
|
|
109
110
|
current_weight += [item.fetch("weight", 1).to_i, 1].max
|
|
@@ -201,6 +202,36 @@ module Text
|
|
|
201
202
|
end
|
|
202
203
|
end
|
|
203
204
|
|
|
205
|
+
def calculate_weight(context, item)
|
|
206
|
+
weight = item["weight"]
|
|
207
|
+
if weight == "*"
|
|
208
|
+
reference = item["segments"]&.find { |s| s["type"] == "reference" }
|
|
209
|
+
key = reference&.fetch("text")
|
|
210
|
+
weight = lookup_and_count_items(context, key)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
weight.to_i < 1 ? 1 : weight.to_i
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def lookup_and_count_items(context, key)
|
|
217
|
+
return 0 unless key
|
|
218
|
+
|
|
219
|
+
builder = context.store.fetch(key)
|
|
220
|
+
return 0 unless builder
|
|
221
|
+
|
|
222
|
+
builder["items"]&.size || 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def calculate_total_weight(context, items)
|
|
226
|
+
sum = 0
|
|
227
|
+
items.each do |item|
|
|
228
|
+
weight = calculate_weight(context, item)
|
|
229
|
+
item["weight"] = weight
|
|
230
|
+
sum += weight
|
|
231
|
+
end
|
|
232
|
+
sum
|
|
233
|
+
end
|
|
234
|
+
|
|
204
235
|
def random_from_dice(text)
|
|
205
236
|
rolled = DiceNomShim.roll(text)
|
|
206
237
|
parsed = JSON.parse(rolled).first["lhs"]
|
data/lib/text/gen/store.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Text
|
|
|
6
6
|
# to save time on database lookups or transformations.
|
|
7
7
|
class Store
|
|
8
8
|
NOT_FOUND_BUILDER = {
|
|
9
|
+
"strategy" => "sample",
|
|
9
10
|
"filters" => [],
|
|
10
11
|
"meta" => {},
|
|
11
12
|
"items" => []
|
|
@@ -33,12 +34,30 @@ module Text
|
|
|
33
34
|
builder = find(key)
|
|
34
35
|
return builder if builder
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
keys = key.split("+")
|
|
38
|
+
builders = keys.map do |key|
|
|
39
|
+
builder = @lookup.call(key)
|
|
40
|
+
builder = not_found(key) unless builder
|
|
41
|
+
builder
|
|
42
|
+
end
|
|
38
43
|
|
|
44
|
+
builder = merge_builders(builders) if builders.length > 1
|
|
39
45
|
add(key, builder.merge("key" => key.to_s.downcase))
|
|
40
46
|
end
|
|
41
47
|
|
|
48
|
+
def merge_builders(builders)
|
|
49
|
+
all_filters = builders.map { |b| b["filters"] }.flatten
|
|
50
|
+
all_meta = builders.inject({}) { |b, acc| acc = Text::Gen::Meta.merge_meta(acc, b["meta"]) }
|
|
51
|
+
all_items = builders.map { |b| b["items"] }.flatten
|
|
52
|
+
strategy = builders.all? {|b| b["strategy"] == "weighted" } ? "weighted" : "sample"
|
|
53
|
+
{
|
|
54
|
+
"strategy" => strategy,
|
|
55
|
+
"filters" => all_filters,
|
|
56
|
+
"meta" => all_meta,
|
|
57
|
+
"items" => all_items,
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
42
61
|
def not_found(key)
|
|
43
62
|
hsh = NOT_FOUND_BUILDER.dup
|
|
44
63
|
hsh["key"] = key
|
data/lib/text/gen/version.rb
CHANGED