text-gen 0.12.6 → 0.13.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96d238eb66df5e222ca033cbdcbc4c96a24756e9d3a24b0fedaa3f8b8c71af7f
4
- data.tar.gz: 2310d7a4f9b6d290d75fefe757be1a766198bed7d5fa6835327500f7f6c11171
3
+ metadata.gz: 5581ccb42efa3604510e17f8af69e6d5b804cc60702f6e1f7001cebf8def8c54
4
+ data.tar.gz: 840ffc9473992cb64850991e4ee442c774acee38ddbdf40e81257be9c69b756d
5
5
  SHA512:
6
- metadata.gz: 24b4e5056f75e5c862ccf4ae21638af665a7ceb0f0a1bdc739ee0ce95f856b398e61a09cc450ed3e5e2329c2c7052765dff3e7ab8daf0dd0c87f91bbe754acff
7
- data.tar.gz: 4c2b802d548be347b3e674fc174648554a4627944f600a30cd9dca3e6806aa014e71952221db0f0fcd329d154151c6583afdb51d40cf05b564bbb838457a0edf
6
+ metadata.gz: 511f34f2a2a29d8c50a85d8b63f504b420047db4e219783525ab855ccd41582b6e9673b3f02bcbb5c2ea1ed9fdf515160a5a6e3b18b251686f55cf0ee3cb10fd
7
+ data.tar.gz: 1b74d7812ff49c5026afd623630435921b7f366c754388439e628a00e0766919458cebf6a22f93bd914f307d8016138b1bcab15104cb01bac3cc132c454d5eb5
data/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
- ## [Unreleased]
1
+ ## [0.13.0] - 2026-06-20
2
+
3
+ - Builder items can be concatonated by using the `+` operator (e.g. `key1+key2`)
4
+ - Cleaned up the strategy behavior to default to "weigted" and renamed "random" to "sample"
2
5
 
3
6
  ## [0.1.0] - 2025-12-28
4
7
 
@@ -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["strategy"] || "random").split(":", 2)
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 || "random"
64
+ @strategy_stack.last&.first || "weighted"
54
65
  end
55
66
 
56
67
  def current_modifier
@@ -12,7 +12,7 @@ module Text
12
12
  end
13
13
 
14
14
  def component_key
15
- "fn(#{to_s})"
15
+ "fn(#{self})"
16
16
  end
17
17
 
18
18
  # Shared utilities
@@ -33,8 +33,9 @@ module Text
33
33
  end
34
34
 
35
35
  def to_s
36
- return "#{ type }:#{ key }:#{ value }" if value
37
- return "#{ type }:#{ key }" if key
36
+ return "#{type}:#{key}:#{value}" if value
37
+ return "#{type}:#{key}" if key
38
+
38
39
  type
39
40
  end
40
41
 
@@ -52,9 +52,13 @@ module Text
52
52
 
53
53
  def pluralize_by_key(str, multiplier, result)
54
54
  tokens = tokenize(str)
55
- word_tokens = tokens.select { |t| t.match?(/\w/) }
55
+ word_tokens = tokens.grep(/\w/)
56
56
  idx = word_tokens.rindex { |s| to_num(s) }
57
- num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
57
+ num = if multiplier > 1
58
+ multiplier
59
+ else
60
+ (idx ? to_num(word_tokens[idx]) : nil)
61
+ end
58
62
  return str if num.nil? || num <= 1
59
63
 
60
64
  component = find_component(result, key)
@@ -68,7 +72,8 @@ module Text
68
72
  return str unless last_word_idx
69
73
 
70
74
  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)
75
+ component_tokens[last_word_idx] =
76
+ exceptions(dc) || single_letter(dc) || others(dc) || ends_with_y(dc) || simple(dc)
72
77
  str.sub(component_text, component_tokens.join)
73
78
  end
74
79
 
@@ -84,9 +89,13 @@ module Text
84
89
 
85
90
  def substitute(str, multiplier = 1)
86
91
  tokens = tokenize(str)
87
- word_tokens = tokens.select { |t| t.match?(/\w/) }
92
+ word_tokens = tokens.grep(/\w/)
88
93
  idx = word_tokens.rindex { |s| to_num(s) }
89
- num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
94
+ num = if multiplier > 1
95
+ multiplier
96
+ else
97
+ (idx ? to_num(word_tokens[idx]) : nil)
98
+ end
90
99
  return str if num.nil? || num <= 1
91
100
 
92
101
  tokens.map! { |t| t.match?(/\A#{Regexp.escape(key)}\z/i) ? value : t }
@@ -97,13 +106,17 @@ module Text
97
106
  return str if str.empty?
98
107
 
99
108
  tokens = tokenize(str)
100
- word_tokens = tokens.select { |t| t.match?(/\w/) }
109
+ word_tokens = tokens.grep(/\w/)
101
110
  return str if word_tokens.length < 2
102
111
 
103
112
  idx = word_tokens.rindex { |s| to_num(s) }
104
113
 
105
114
  # Use the multiplier if available, otherwise parse from text
106
- num = multiplier > 1 ? multiplier : (idx ? to_num(word_tokens[idx]) : nil)
115
+ num = if multiplier > 1
116
+ multiplier
117
+ else
118
+ (idx ? to_num(word_tokens[idx]) : nil)
119
+ end
107
120
 
108
121
  return str if num.nil? || num <= 1
109
122
  return str if idx && idx >= word_tokens.length - 1
@@ -4,7 +4,7 @@ module Text
4
4
  module Gen
5
5
  module Filter
6
6
  class Replace < Base
7
- def builder(context, builder)
7
+ def builder(_context, builder)
8
8
  return builder unless builder["key"] == key
9
9
 
10
10
  Result.new(text: value, type: "replace")
@@ -14,7 +14,7 @@ module Text
14
14
  def build(hsh_or_key, depth)
15
15
  hsh = hsh_or_key.is_a?(Hash) ? hsh_or_key : key_to_hsh(hsh_or_key)
16
16
  filter_class = available_filters.find { |f| f.filter_key == hsh["type"] }
17
- filter_class.new(hsh, depth) if filter_class
17
+ filter_class&.new(hsh, depth)
18
18
  end
19
19
 
20
20
  def key_to_hsh(key)
data/lib/text/gen/meta.rb CHANGED
@@ -33,7 +33,7 @@ module Text
33
33
  else
34
34
  hsh.each_key do |k|
35
35
  if hsh.key?(k)
36
- new_hsh[k] = k == key ? hsh[k].select { |v| v != val } : hsh[k].dup
36
+ new_hsh[k] = k == key ? hsh[k].reject { |v| v == val } : hsh[k].dup
37
37
  end
38
38
  end
39
39
  end
@@ -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
- run_random_item(context, items)
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 = items.sum { |item| [item.fetch("weight", 1).to_i, 1].max }
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)
@@ -142,10 +142,10 @@ module Text
142
142
  return if results.any?(&:nil?) || results.empty?
143
143
 
144
144
  result = Result.merge(results,
145
- value: item["value"],
146
- multiplier: item["multiplier"],
147
- meta: item["meta"],
148
- type: context.current_key)
145
+ value: item["value"],
146
+ multiplier: item["multiplier"],
147
+ meta: item["meta"],
148
+ type: context.current_key)
149
149
  context.apply_result_filters(result)
150
150
  end
151
151
  end
@@ -201,6 +201,36 @@ module Text
201
201
  end
202
202
  end
203
203
 
204
+ def calculate_weight(context, item)
205
+ weight = item["weight"]
206
+ if weight == "*"
207
+ reference = item["segments"]&.find { |s| s["type"] == "reference" }
208
+ key = reference&.fetch("text")
209
+ weight = lookup_and_count_items(context, key)
210
+ end
211
+
212
+ [weight.to_i, 1].max
213
+ end
214
+
215
+ def lookup_and_count_items(context, key)
216
+ return 0 unless key
217
+
218
+ builder = context.store.fetch(key)
219
+ return 0 unless builder
220
+
221
+ builder["items"]&.size || 1
222
+ end
223
+
224
+ def calculate_total_weight(context, items)
225
+ sum = 0
226
+ items.each do |item|
227
+ weight = calculate_weight(context, item)
228
+ item["weight"] = weight
229
+ sum += weight
230
+ end
231
+ sum
232
+ end
233
+
204
234
  def random_from_dice(text)
205
235
  rolled = DiceNomShim.roll(text)
206
236
  parsed = JSON.parse(rolled).first["lhs"]
@@ -4,7 +4,7 @@ module Text
4
4
  module Gen
5
5
  module Segment
6
6
  class Reference
7
- REFERENCE_MATCHER = /\[\s*([a-z0-9-]+(?::[a-z0-9-]+)*)\s*\]/
7
+ REFERENCE_MATCHER = /\[\s*([a-z0-9-]+(?:(?:>>|>|:)[a-z0-9-]+)*)\s*\]/
8
8
  class << self
9
9
  def scan(scanner)
10
10
  str = scanner.scan(REFERENCE_MATCHER)
@@ -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
- builder = @lookup.call(key)
37
- return not_found(key) unless builder
37
+ keys = key.split("+")
38
+ builders = keys.map do |key|
39
+ builder = @lookup.call(key)
40
+ builder ||= not_found(key)
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| 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Text
4
4
  module Gen
5
- VERSION = "0.12.6"
5
+ VERSION = "0.13.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: text-gen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.6
4
+ version: 0.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - G Palmer