text-gen 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a797ffbcfad9d7ad94943ea650e8fa0fa92f88e305035b2085d9500849f3bd87
4
- data.tar.gz: dbf62805408bd3258a0153d83a716e29d650d8f7c37cefaa2b9090a6518f3355
3
+ metadata.gz: fd025671968a7f3a6d7fd3dc9cc0cb1d02f06a43054c2aa8395f6db054d86c79
4
+ data.tar.gz: 5ff622ee28e61931406c4d3f1d792c1bc3eefde351c2bd8e22f8b285b715158d
5
5
  SHA512:
6
- metadata.gz: 0a5df8167670c236ead54c4c9cb63abe9fd8340c46f0bda70eac50255189ca0aa21ac6cf6305ddd451f2d10ef3af0314262cd48955f801f4b8e1e7fa1cff0d7b
7
- data.tar.gz: 7b1035f58a41888f6e48b3ae56cb948d49eed2b549128fb333d95f7df2686d5831156d4cfd3b271813cb44a387d406eadcfed6cf507e8236d98636b043ac752c
6
+ metadata.gz: eafd1508e2cac903b0f81e0c94dd47eee86d8bc8321d863f3258b5d2e730cc016a84b7cbba48e0827ba9fc6545916cd20a521d7927587eedfd017cd6561f4b35
7
+ data.tar.gz: 0d82d9c14341a74baea22e17cb1830f3c61559a33ca8049b0ba5fde856552ddde0a5fddb82b67b0d81a4d80505f6e8aa8894b930640d49281127a27e20d1148d
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find:*)"
5
+ ]
6
+ }
7
+ }
@@ -1,6 +1,7 @@
1
1
  module Text
2
2
  module Gen
3
3
  class Filter
4
+ SEPARATORS = { "tab" => "\t", "newline" => "\n", "space" => " " }.freeze
4
5
  class << self
5
6
  def functions(result, filters)
6
7
  return result if filters.nil? || filters.empty?
@@ -63,6 +64,26 @@ module Text
63
64
  nil
64
65
  end
65
66
 
67
+ # Create a builder that always returns a constant value
68
+ def constant_builder(key, str)
69
+ # TODO: the string should be parsed into segments
70
+ segments = Segment::Parser.parse(str)
71
+ {
72
+ "key" => key,
73
+ "items" => [
74
+ {
75
+ "segments" => segments,
76
+ "value" => 0,
77
+ "multiplier" => 1,
78
+ "filters" => [],
79
+ "meta" => {}
80
+ }
81
+ ],
82
+ "meta" => {},
83
+ "filters" => []
84
+ }
85
+ end
86
+
66
87
  # Replace locale is used on an item; if the item is selected and has
67
88
  # associated meta value of "locale" and the request is using that locale value
68
89
  # then replace the item text with the value of the meta. If there is
@@ -93,16 +114,7 @@ module Text
93
114
  return "" unless separator_filter
94
115
  return separator_filter["value"] if separator_filter["key"] == "string"
95
116
 
96
- case separator_filter["key"]
97
- when "tab"
98
- "\t"
99
- when "space"
100
- " "
101
- when "newline"
102
- "\n"
103
- else
104
- ""
105
- end
117
+ SEPARATORS.fetch(separator_filter["key"], "")
106
118
  end
107
119
 
108
120
  def filter_by_type(filters, type)
@@ -0,0 +1,42 @@
1
+ module Text
2
+ module Gen
3
+ # ResultAccumulator handles accumulating results with attempt tracking
4
+ class ResultAccumulator
5
+ class << self
6
+ def accumulate(unique:, count:, max_attempts:, &block)
7
+ new(unique: unique, count: count, max_attempts: max_attempts).accumulate(&block)
8
+ end
9
+ end
10
+
11
+ def initialize(unique:, count:, max_attempts:)
12
+ @unique = unique
13
+ @count = count
14
+ @max_attempts = max_attempts
15
+ @results = unique ? {} : []
16
+ @attempts = max_attempts * count
17
+ end
18
+
19
+ def accumulate(&block)
20
+ while @results.size < @count
21
+ result = block.call
22
+ add_result(result) if result
23
+
24
+ @attempts -= 1
25
+ raise MaxAttemptsError if @attempts.zero?
26
+ end
27
+
28
+ @results.map { |_, v| v }
29
+ end
30
+
31
+ private
32
+
33
+ def add_result(result)
34
+ if @unique
35
+ @results[result.text] = result
36
+ else
37
+ @results << [result.text, result]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -5,103 +5,35 @@ require "json"
5
5
 
6
6
  module Text
7
7
  module Gen
8
- # ResultAccumulator handles accumulating results with attempt tracking
9
- class ResultAccumulator
10
- class << self
11
- def accumulate(unique:, count:, max_attempts:, &block)
12
- new(unique: unique, count: count, max_attempts: max_attempts).accumulate(&block)
13
- end
14
- end
15
-
16
- def initialize(unique:, count:, max_attempts:)
17
- @unique = unique
18
- @count = count
19
- @max_attempts = max_attempts
20
- @results = unique ? {} : []
21
- @attempts = max_attempts * count
22
- end
23
-
24
- def accumulate(&block)
25
- while @results.size < @count
26
- result = block.call
27
- add_result(result) if result
28
-
29
- @attempts -= 1
30
- raise MaxAttemptsError if @attempts.zero?
31
- end
32
-
33
- @results.map { |_, v| v }
34
- end
35
-
36
- private
37
-
38
- def add_result(result)
39
- if @unique
40
- @results[result.text] = result
41
- else
42
- @results << [result.text, result]
43
- end
44
- end
45
- end
46
-
47
- # AltLookup handles alternative filter lookups
48
- class AltLookup
49
- def initialize(request_filters)
50
- @filters = {}
51
- populate(request_filters)
52
- end
53
-
54
- def key?(key)
55
- @filters.key?(key)
56
- end
57
-
58
- def [](key)
59
- @filters[key]
60
- end
61
-
62
- private
63
-
64
- def populate(request_filters)
65
- request_filters.each do |f|
66
- next unless f["type"] == "alt"
67
-
68
- k = f["key"]
69
- v = f["value"]
70
- raise FilterError, "Invalid alt filter" if k.nil? || v.nil? || k.empty?
71
-
72
- (@filters[k.to_s.downcase] ||= []) << v
73
- end
74
- end
75
- end
76
-
77
8
  # Runner generates results from the builder that is found with the given key.
78
9
  class Runner
79
- attr_reader :key, :locale, :lookup, :store, :max_attempts, :max_recursion
80
-
81
- def initialize(
82
- key:,
83
- lookup:,
84
- locale: nil,
85
- unique: false,
86
- max_recursion: 10,
87
- max_attempts: 10,
88
- request_filters: []
89
- )
10
+ attr_reader :key, :lookup, :max_attempts, :max_recursion
11
+ attr_accessor :unique, :locale, :store
12
+
13
+ def initialize(key:, lookup:, max_recursion: 10, max_attempts: 10, request_filters: [])
90
14
  @key = key.to_s.downcase
91
15
  @lookup = lookup
92
- @locale = locale
16
+ @locale = nil
93
17
  @store = Store.new
94
- @unique = unique
18
+ @unique = false
95
19
  @request_filters = request_filters
96
20
  @max_attempts = max_attempts
97
21
  @max_recursion = max_recursion
98
- @alt_lookup = AltLookup.new(request_filters)
22
+ populate_replace
99
23
  end
100
24
 
101
25
  def unique?
102
26
  @unique
103
27
  end
104
28
 
29
+ # A filter of type "replace:key:value" creates a temporary builder
30
+ # that always returns the given value.
31
+ def populate_replace
32
+ @request_filters.each do |f|
33
+ store.add(f["key"], Filter.constant_builder(f["key"], f["value"])) if f["type"] == "replace"
34
+ end
35
+ end
36
+
105
37
  def run(count: 1)
106
38
  builder = fetch_builder(key)
107
39
 
@@ -111,8 +43,8 @@ module Text
111
43
  end
112
44
 
113
45
  def fetch_builder(key)
114
- builder = store.find(key) || lookup.call(key)
115
- store.add(key, builder)
46
+ builder = @store.find(key) || lookup.call(key)
47
+ @store.add(key, builder)
116
48
  builder
117
49
  end
118
50
 
@@ -121,9 +53,6 @@ module Text
121
53
  depth += 1
122
54
  raise MaxRecursionError if depth > max_recursion
123
55
 
124
- alt_result = alt_result_for(key, builder)
125
- return alt_result if alt_result
126
-
127
56
  current_filters = merge_filters(builder, filters)
128
57
  current_items = apply_item_filters(builder["items"], current_filters)
129
58
 
@@ -233,12 +162,6 @@ module Text
233
162
  items
234
163
  end
235
164
 
236
- def alt_result_for(key, builder)
237
- return nil unless @alt_lookup.key?(builder["key"])
238
-
239
- Result.new(text: @alt_lookup[key], type: :alt, meta: builder["meta"])
240
- end
241
-
242
165
  def locale_result_for(item)
243
166
  text = Filter.replace_locale(item["meta"], locale)
244
167
  return nil unless text
@@ -0,0 +1,20 @@
1
+ module Text
2
+ module Gen
3
+ module Segment
4
+ class Constant
5
+ TEXT = /(?:(?:\\\[)|(?:\\\])|[^\[\]])+/
6
+ class << self
7
+ def scan(scanner)
8
+ str = scanner.scan(TEXT)
9
+ return unless str
10
+
11
+ {
12
+ "type" => "constant",
13
+ "text" => str
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Text
2
+ module Gen
3
+ module Segment
4
+ class Dice
5
+ DICE_MATCHER = /\[\s*(\d*[dD]\d+(?:\s*[+x-]\s*\d+)?)\s*\]/
6
+ class << self
7
+ def scan(scanner)
8
+ str = scanner.scan(DICE_MATCHER)
9
+ return unless str
10
+
11
+ {
12
+ "type" => "dice",
13
+ "text" => scanner[1]
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Text
2
+ module Gen
3
+ module Segment
4
+ class Number
5
+ NUMBER_MATCHER = /([+-]?\d+)/
6
+ class << self
7
+ def scan(scanner)
8
+ str = scanner.scan(NUMBER_MATCHER)
9
+ return unless str
10
+
11
+ {
12
+ "type" => "constant",
13
+ "text" => str
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "constant"
2
+ require_relative "dice"
3
+ require_relative "number"
4
+ require_relative "reference"
5
+
6
+ module Text
7
+ module Gen
8
+ module Segment
9
+ class Parser
10
+ class SegmentParseError < StandardError; end
11
+ class << self
12
+ def parse(str)
13
+ segments = []
14
+ scanner = StringScanner.new(str)
15
+ until scanner.eos?
16
+ seg = Dice.scan(scanner) || Reference.scan(scanner) || Number.scan(scanner) || Constant.scan(scanner)
17
+ raise SegmentParseError, "Invalid segment: #{scanner.rest}" unless seg
18
+
19
+ segments << seg
20
+ end
21
+ segments
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ module Text
2
+ module Gen
3
+ module Segment
4
+ class Reference
5
+ REFERENCE_MATCHER = /\[\s*([a-z0-9-]+(?::[a-z0-9i]+)*)\s*\]/
6
+ class << self
7
+ def scan(scanner)
8
+ str = scanner.scan(REFERENCE_MATCHER)
9
+ return unless str
10
+
11
+ {
12
+ "type" => "reference",
13
+ "text" => scanner[1],
14
+ "filters" => [],
15
+ "meta" => {}
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Text
4
4
  module Gen
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/text/gen.rb CHANGED
@@ -6,8 +6,10 @@ require_relative "gen/lookup_error"
6
6
  require_relative "gen/max_attempts_error"
7
7
  require_relative "gen/max_recursion_error"
8
8
 
9
+ require_relative "gen/result_accumulator"
9
10
  require_relative "gen/titleizer"
10
11
  require_relative "gen/result"
12
+ require_relative "gen/segment/parser"
11
13
  require_relative "gen/filter"
12
14
  require_relative "gen/store"
13
15
  require_relative "gen/runner"
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - G Palmer
@@ -33,6 +33,7 @@ executables: []
33
33
  extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
36
+ - ".claude/settings.local.json"
36
37
  - CHANGELOG.md
37
38
  - CODE_OF_CONDUCT.md
38
39
  - LICENSE.txt
@@ -45,7 +46,13 @@ files:
45
46
  - lib/text/gen/max_attempts_error.rb
46
47
  - lib/text/gen/max_recursion_error.rb
47
48
  - lib/text/gen/result.rb
49
+ - lib/text/gen/result_accumulator.rb
48
50
  - lib/text/gen/runner.rb
51
+ - lib/text/gen/segment/constant.rb
52
+ - lib/text/gen/segment/dice.rb
53
+ - lib/text/gen/segment/number.rb
54
+ - lib/text/gen/segment/parser.rb
55
+ - lib/text/gen/segment/reference.rb
49
56
  - lib/text/gen/store.rb
50
57
  - lib/text/gen/titleizer.rb
51
58
  - lib/text/gen/version.rb