drawght 1.0.0 → 1.1.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: 8a5dd8e8d4c11181d223311d1db2e25d8d6a9e7065289fb2ab40de86ce4fd054
4
- data.tar.gz: c9698b4604f71e915915bc57ceefd595a21e36fa1969ea0d40e0f6fb907dbed9
3
+ metadata.gz: 9ed4619bbd991ccf58bd7e7c89407879181c656e98667b933a25cdf751a9c273
4
+ data.tar.gz: ae541e430afcc2ee112cd78d0549767845fbb1a8def0204de1d29e4db14baef9
5
5
  SHA512:
6
- metadata.gz: f0e6771bf26dbacaf3d3c62dbfa947e87beb639cdad189b3c068227940a1c6410a25de09a6632b450f3128d924f9a32e90ec99910dc5e1fec803abc93f016801
7
- data.tar.gz: 201f2fb4ad4de65f40d79f50d97da5244d826a18718e4bec2c84ee3935247fc0f0be49e34cc98a1d7867bb418afc90650c8b3e2aa38442684ff29460265d2b53
6
+ metadata.gz: 704e351d12da61369698dd568740d3943ee9542ac01c3d5e52c9bad514063ea6e1d395ba503df518a91605d44fb1bb7df0b68893527fa036ea74132d6680cafa
7
+ data.tar.gz: e10c78a814d001c3c7baa5eec7b748c5e34256b4a1dc7206d109372ec815279ba658f2ed1017838e7e233bf62befbe847b9df1af9fa4a4bc7b74090c9651b40a
data/CHANGELOG.yaml CHANGED
@@ -1,3 +1,12 @@
1
+ - version: 1.1.0
2
+ date: 2025-12-22
3
+ summary:
4
+ Syntax validation and new expression.
5
+ changes:
6
+ - Syntax validation.
7
+ - Refined extension methods have been moved to their respective modules.
8
+ - Syntax for getting the length of a collection.
9
+
1
10
  - version: 1.0.0
2
11
  date: 2025-12-03
3
12
  summary:
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Drawght
2
2
 
3
- Drawght is a data handler for texts without logical statements. The goal is
4
- to use a dataset (such as the subject of a text) to draft a document
5
- template. It can be considered a mini template processor.
3
+ Drawght (like draft/draught) is a data handler for texts without logical
4
+ statements. The goal is to use a dataset (such as the subject of a text) to
5
+ draft a document template. It can be considered a mini template processor.
6
6
 
7
7
  Data is accessed through `{}` braces, replaced by their respective values.
8
8
 
@@ -196,22 +196,63 @@ puts result
196
196
 
197
197
  Drawght has a simple syntax:
198
198
 
199
- - `{key}`: converts `key` to its respective value. If the value is a
200
- collection, then the row will be replicated and converted with the respective
201
- values.
199
+ - `{identifier}`: converts `identifier` to its respective value. If the value
200
+ is a collection, then the row will be replicated and converted with the
201
+ respective values.
202
202
 
203
- - `{object.key}`: converts `key` to its respective value inside `object`,
204
- assuming the same behavior as `{key}`.
203
+ - `{structure.attribute}`: converts `attribute` to its respective value inside
204
+ `structure`, assuming the same behavior as `{identifier}`.
205
205
 
206
- - `{collection:key}`: selects `collection`, replicates the line for each item
207
- in the collection, and converts `key` to its respective value contained in an
208
- object within `collection`. The `collection` key can also be accessed by
209
- `object.collection`, just as `key` can also be accessed by `object.key` which
210
- will be converted following the same process in case it is a collection.
206
+ - `{collection:attribute}`: selects `collection`, replicates the line for each
207
+ item in the collection, and converts `attribute` to its respective value
208
+ contained in an structure within `collection`. The `collection` key can also
209
+ be accessed by `structure.collection`, just as `attribute` can also be
210
+ accessed by `structure.attribute` which will be converted following the same
211
+ process in case it is a collection.
211
212
 
212
- - `{collection#n.key}`: selects item object `n` (from 1) from `collection` and
213
- converts `key` to its respective value. The whole process is similar to
214
- `object.key`.
213
+ - `{collection#nth.attribute}`: selects the `nth` (start from 1) item from
214
+ `collection` and converts `attribute` to its respective value. The whole
215
+ process is similar to `structure.attribute`.
216
+
217
+ - `{collection#$.attribute}`: selects the `nth` (start from 1) item from
218
+ `collection` and converts `attribute` to its respective value. The whole
219
+ process is similar to `structure.attribute`.
220
+
221
+ ### EBNF
222
+
223
+ ```ebnf
224
+ identifier ::= initial_name { compound_name | space compound_name } ;
225
+
226
+ initial_name ::= letter | "_" ;
227
+
228
+ compound_name ::= letter | digit | "_" | "-" ;
229
+
230
+ letter ::= "A"…"Z" | "a"…"z" ;
231
+
232
+ digit ::= "0"…"9" ;
233
+
234
+ space ::= " " ;
235
+
236
+ expression ::= path [ length ] ;
237
+
238
+ path ::= [ structural_path ] { sequential_path } ;
239
+
240
+ structural_path ::= attribute { "." attribute } ;
241
+
242
+ scoped_path ::= ":" structural_path ;
243
+
244
+ attribute ::= element | item ;
245
+
246
+ element ::= identifier [ index ] ;
247
+
248
+ item ::= index ;
249
+
250
+ index ::= "#" ( number | "$" ) ;
251
+
252
+ number ::= digit { digit } ;
253
+
254
+ length ::= "#&" ;
255
+ ```
215
256
 
216
257
  ## How it works
217
258
 
@@ -3,10 +3,31 @@
3
3
 
4
4
  module Drawght
5
5
 
6
+ module HashExtensions
7
+ refine Hash do
8
+ def deep_stringify_keys!
9
+ transform_keys! do |key|
10
+ case value = fetch(key)
11
+ when Hash then value.deep_stringify_keys!
12
+ when Array then
13
+ value.map! do |item|
14
+ (item.is_a? Hash) ? item.deep_stringify_keys! : item
15
+ end
16
+ end
17
+
18
+ key.to_s
19
+ end
20
+
21
+ self
22
+ end
23
+ end
24
+ end
25
+
6
26
  class Compiler
7
- using Extensions
27
+ using HashExtensions
8
28
 
9
29
  include Parser
30
+ include Tracker
10
31
 
11
32
  attr_reader :template, :dataset, :result
12
33
 
@@ -30,11 +51,11 @@ class Compiler
30
51
 
31
52
  def convert
32
53
  lines = template.lines.map do |line|
33
- next line unless line.match? PLACEHOLDERS_PATTERN
54
+ next line unless has_placeholders? line
34
55
 
35
56
  mapping_placeholders_from line
36
57
 
37
- newline = convert_straightly_placeholders_from line
58
+ newline = convert_structural_placeholders_from line
38
59
 
39
60
  convert_sequential_placeholders_from newline
40
61
  end
@@ -42,12 +63,12 @@ class Compiler
42
63
  result.replace lines.join
43
64
  end
44
65
 
45
- def convert_straightly_placeholders_from string
46
- straightly_placeholders.reduce string.dup do |template, holding|
47
- matter = dataset.ditch *pathkeys_from(holding)
66
+ def convert_structural_placeholders_from string
67
+ structural_placeholders.reduce string.dup do |template, expression|
68
+ matter = pathing dataset, *pathkeys_from(expression)
48
69
 
49
70
  converted = [matter].flatten.map do |value|
50
- template.dup.gsub! holding.to_placeholder, value.to_s
71
+ template.dup.gsub! pathize(expression), value.to_s
51
72
  end
52
73
 
53
74
  template.replace converted.join
@@ -56,12 +77,12 @@ class Compiler
56
77
 
57
78
  def convert_sequential_placeholders_from string
58
79
  sequential_placeholders.reduce string.dup do |template, (attribute, mappings)|
59
- matter = dataset.ditch *pathkeys_from(attribute)
80
+ matter = pathing dataset, *pathkeys_from(attribute)
60
81
 
61
82
  converted = matter.map do |values|
62
- mappings.inject template.dup do |partial, (holding, key)|
63
- value = values.ditch(*pathkeys_from(key)) || key
64
- partial.dup.gsub! holding.to_placeholder, value.to_s
83
+ mappings.inject template.dup do |partial, (expression, key)|
84
+ value = pathing(values, *pathkeys_from(key)) || key
85
+ partial.dup.gsub! pathize(expression), value.to_s
65
86
  end
66
87
  end
67
88
 
@@ -2,22 +2,126 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Drawght
5
+ module ArrayExtensions
6
+ refine Array do
7
+ def add_unique item
8
+ push item unless include? item
9
+ self
10
+ end
11
+ end
12
+ end
13
+
5
14
  module Parser
6
- using Extensions
7
-
8
- def pathkeys_from placeholder
9
- path = case placeholder
10
- when ATTRIBUTES_PATTERN then
11
- path_keys_for_attribute placeholder
12
- when QUERY_PATTERN then
13
- path_keys_for_query placeholder
14
- else
15
- [placeholder]
16
- end
15
+ using ArrayExtensions
16
+
17
+ TOKENS = [
18
+ PREFIX = '{',
19
+ SUFFIX = '}',
20
+ ACCESSOR = '.',
21
+ INDEX = '#',
22
+ SCOPE = ':',
23
+ CONTEXT = '@',
24
+ LAST = '$',
25
+ LENGTH = '&',
26
+ ]
27
+
28
+ # Identifiers/name matches:
29
+ #
30
+ # - "name"
31
+ # - "variable name"
32
+ # - "variable-name"
33
+ # - "variable_name"
34
+ # - "_name_"
35
+ # - "name_"
36
+ # - "_name"
37
+ IDENTIFIER_MATCHES = "[A-Za-z_][A-Za-z0-9_\\-]*(?: [A-Za-z0-9_\\-]+)*"
38
+
39
+ # Index/item matches:
40
+ #
41
+ # - "#1" for first item.
42
+ # - "#n" for n-th item.
43
+ # - "#$" for last item.
44
+ INDEX_MATCHES = "\\#{INDEX}(?:\\d+|\\#{LAST})"
45
+
46
+ # Element matches:
47
+ #
48
+ # - "items#1" for first named item.
49
+ # - "items#n" for n-th named item.
50
+ # - "items#$" for last named item.
51
+ #
52
+ # Matches with IDENTIFIER_MATCHES and INDEX_MATCHES.
53
+ ELEMENT_MATCHES = "#{IDENTIFIER_MATCHES}(?:#{INDEX_MATCHES})?"
54
+
55
+ # Attribute matches.
56
+ ATTRIBUTE_MATCHES = "\\#{ACCESSOR}(?:#{ELEMENT_MATCHES})"
57
+
58
+ # Validation
59
+ #
60
+ # Structural matches:
61
+ #
62
+ # - "struct.attribute" for attribute value from struct.
63
+ # - "struct.substruct.attribute" for attribute value of the substruct from
64
+ # struct.
65
+ # - "struct.items#1" for first item value in items of the struct.
66
+ # - "struct.items#1.attribute" for attribute value of the first item in
67
+ # items of the struct.
68
+ # - "#1.attribute" for attribute value of the first item.
69
+ # - "#1.struct.attribute" for attribute value of the struct from the first item.
70
+ # - "items#1.attribute" for attribute value of the first item in items.
71
+ # - "items#1.struct.attribute" for attribute value of the struct of the
72
+ # first item in items.
73
+ # - "items#1.subitems#1" for subitem value of the first item in items.
74
+ # - "items#1.subitems#1.attribute" for attribute value of the subitem from
75
+ # first item in items.
76
+ #
77
+ # Scoped matches:
78
+ #
79
+ # ":attribute"
80
+ # ":collection:attribute"
81
+ # "collection:attribute"
82
+ # "collection:subcollection:attribute"
83
+ # "items#1:attribute"
84
+ # "struct.collection:attribute"
85
+ # "items#1.collection:attribute"
86
+ # "items#1.subitems#1:attribute"
87
+ # "items#1.subitems#1.collection:attribute"
88
+ # "struct.collection:noitcelloc.tcurts:attribute"
89
+ VALIDATION_PATTERN = Regexp.new "^(?=.*[A-Za-z_\\#{INDEX}])(?:(?:#{ELEMENT_MATCHES}|#{INDEX_MATCHES})(?:#{ATTRIBUTE_MATCHES})*)?(?:\\#{SCOPE}(?:#{ELEMENT_MATCHES})(?:#{ATTRIBUTE_MATCHES})*)*(?:\\#{INDEX}\\#{LENGTH})?$"
90
+
91
+ PLACEHOLDERS_PATTERN = Regexp.new "\\#{PREFIX}([^\\#{SUFFIX}]+)\\#{SUFFIX}"
92
+ STRUCTURE_TOKEN_PATTERN = Regexp.new "(\\#{ACCESSOR}|\\#{INDEX}|\\#{LENGTH}$)"
93
+ SCOPE_TOKEN_PATTERN = Regexp.new "(\\#{SCOPE})"
94
+ SCOPE_PATH_PATTERN = Regexp.new "^(.*)#{SCOPE_TOKEN_PATTERN}(.*)$"
95
+
96
+ NUMBER_PATTERN = /^\d+/
97
+
98
+ using ArrayExtensions
99
+
100
+ class SyntaxError < StandardError
101
+ def initialize syntax
102
+ super "Invalid syntax for \"#{syntax}\""
103
+ end
104
+ end
105
+
106
+ def pathkeys_from string
107
+ raise SyntaxError.new string unless syntax_valid? string
108
+
109
+ path = case string
110
+ when STRUCTURE_TOKEN_PATTERN then
111
+ pathkeys_for_structure string
112
+ when SCOPE_TOKEN_PATTERN then
113
+ pathkeys_for_collection string
114
+ else
115
+ [string]
116
+ end
17
117
 
18
118
  path.flatten
19
119
  end
20
120
 
121
+ def syntax_valid? string
122
+ string.match? VALIDATION_PATTERN
123
+ end
124
+
21
125
  def placeholders_from string
22
126
  return [] unless string =~ PLACEHOLDERS_PATTERN
23
127
 
@@ -28,61 +132,70 @@ module Drawght
28
132
  clear_placeholder_mappings!
29
133
 
30
134
  placeholders_from(string).map do |placeholder|
31
- if placeholder =~ QUERY_PATTERN
32
- placeholder.scan %r/^(.*)#{QUERY}(.*)$/ do |pathkeys, attribute|
135
+ if placeholder =~ SCOPE_TOKEN_PATTERN
136
+ placeholder.scan SCOPE_PATH_PATTERN do |pathkeys, _delimiter, attribute|
33
137
  (sequential_placeholders[pathkeys] ||= {}).update placeholder => attribute
34
138
  end
35
139
  else
36
- straightly_placeholders.add placeholder
140
+ structural_placeholders.add_unique placeholder
37
141
  end
38
142
 
39
143
  placeholder
40
144
  end
41
145
  end
42
146
 
43
- def straightly_placeholders
44
- @straightly_placeholders ||= []
147
+ def structural_placeholders
148
+ @structural_placeholders ||= []
45
149
  end
46
150
 
47
151
  def sequential_placeholders
48
152
  @sequential_placeholders ||= {}
49
153
  end
50
154
 
51
- def replace_placeholders template, value
52
- case value
53
- when Hash
54
- replace_placeholder_attributes_for template, value
55
- when Array
56
- replace_placeholder_collections_for template, value
57
- else
58
- replace_placeholder_variables_from template, value
59
- end
155
+ def has_placeholders? string
156
+ string.match? PLACEHOLDERS_PATTERN
157
+ end
158
+
159
+ def pathize string
160
+ "#{PREFIX}#{string}#{SUFFIX}"
60
161
  end
61
162
 
62
163
  private
63
164
 
64
- def path_keys_for_attribute placeholder
65
- placeholder.split(ATTRIBUTES_PATTERN).map do |item|
66
- if item.to_s =~ QUERY_PATTERN
67
- path_keys_for_query item
68
- else
69
- item.to_s =~ /^\d/ ? item.to_i - 1 : item unless item.to_s.empty?
70
- end
71
- end.compact
165
+ def pathkeys_for_structure placeholder
166
+ placeholder
167
+ .split(STRUCTURE_TOKEN_PATTERN)
168
+ .reject{ |expression| [ACCESSOR, INDEX].include? expression }
169
+ .map do |expression|
170
+ value = zeroize expression
171
+
172
+ if value =~ SCOPE_TOKEN_PATTERN
173
+ pathkeys_for_collection expression
174
+ else
175
+ value =~ NUMBER_PATTERN ? value.to_i - 1 : expression unless value.empty?
176
+ end
177
+ end.compact
178
+ end
179
+
180
+ def zeroize value, token: LAST
181
+ value.to_s.sub token, '0'
72
182
  end
73
183
 
74
- def path_keys_for_query placeholder
75
- placeholder.gsub(QUERY, "#{QUERY}&#{QUERY}").split(QUERY_PATTERN).map do |item|
76
- if item.to_s =~ ATTRIBUTES_PATTERN
77
- path_keys_for_attribute item
184
+ def pathkeys_for_collection placeholder
185
+ placeholder.split(SCOPE_TOKEN_PATTERN).map do |expression|
186
+ case expression.to_s
187
+ when STRUCTURE_TOKEN_PATTERN then
188
+ pathkeys_for_structure expression
189
+ when SCOPE then
190
+ CONTEXT
78
191
  else
79
- item
192
+ expression
80
193
  end
81
194
  end
82
195
  end
83
196
 
84
197
  def clear_placeholder_mappings!
85
- straightly_placeholders.clear && sequential_placeholders.clear
198
+ structural_placeholders.clear && sequential_placeholders.clear
86
199
  end
87
200
  end
88
201
  end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module Drawght
4
+
5
+ module Tracker
6
+ LENGTH = Parser::LENGTH
7
+ CONTEXT = Parser::CONTEXT
8
+
9
+ def pathing hash, *pathkeys
10
+ if i = pathkeys.index CONTEXT
11
+ prefixes, suffixes = pathkeys.slice(0..i - 1), pathkeys.slice(i + 1..-1)
12
+ result = pathing(hash, *prefixes).map{ |item| pathing item, *suffixes }
13
+ suffixes.last == LENGTH ? result.reduce(&:+) : result
14
+ elsif pathkeys.last == LENGTH
15
+ hash.dig(*pathkeys.slice(0..-2)).length
16
+ else
17
+ hash.dig *pathkeys
18
+ end
19
+ rescue => error
20
+ raise error, "The pathkeys \"#{pathkeys.join ' '}\" is a not valid path"
21
+ end
22
+ end
23
+
24
+ end # Drawght
@@ -1,12 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Drawght
4
- VERSION = "1.0.0"
5
- RELEASE_DATE = "2025-12-03"
4
+ VERSION = "1.1.0"
5
+ RELEASE_DATE = "2025-12-22"
6
6
  CHANGESET = [
7
- "New compiler process.",
8
- "Refined extension methods have been added.",
9
- "Test coverage.",
10
- "Syntax revision for nested values in collections.",
7
+ "Syntax validation.",
8
+ "Refined extension methods have been moved to their respective modules.",
9
+ "Syntax for getting the length of a collection.",
11
10
  ]
12
11
  end
data/lib/drawght.rb CHANGED
@@ -2,69 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Drawght
5
- TOKENS = [
6
- PREFIX = '{',
7
- SUFFIX = '}',
8
- ATTRIBUTE = '.',
9
- ITEM = '#',
10
- QUERY = ':',
11
- ]
12
-
13
- PLACEHOLDERS_PATTERN = Regexp.new "\\#{PREFIX}([^\\#{SUFFIX}]+)\\#{SUFFIX}"
14
-
15
- ATTRIBUTES_PATTERN = Regexp.new "\\#{ATTRIBUTE}|\\#{ITEM}"
16
- ATTRIBUTE_PATTERN = Regexp.new "\\#{ATTRIBUTE}"
17
- ITEM_PATTERN = Regexp.new "\\#{ITEM}"
18
- QUERY_PATTERN = Regexp.new "\\#{QUERY}"
19
-
20
- PATH_PATTERN = Regexp.new "\\#{ATTRIBUTE}|\\#{ITEM}|\\#{QUERY}"
21
-
22
- module Extensions
23
- refine Hash do
24
- def deep_stringify_keys!
25
- transform_keys! do |key|
26
- case value = fetch(key)
27
- when Hash then value.deep_stringify_keys!
28
- when Array then
29
- value.map! do |item|
30
- (item.is_a? Hash) ? item.deep_stringify_keys! : item
31
- end
32
- end
33
-
34
- key.to_s
35
- end
36
-
37
- self
38
- end
39
-
40
- def ditch *pathkeys
41
- if i = pathkeys.index('&')
42
- list, subpath = pathkeys[0..i - 1], pathkeys[i + 1..-1]
43
- dig(*list).map{ |item| item.ditch *subpath }
44
- else
45
- dig *pathkeys
46
- end
47
- rescue => error
48
- error.message += "The pathkeys \"#{pathkeys.join ','}\" is a not valid path"
49
- raise error
50
- end
51
- end
52
-
53
- refine Array do
54
- def add item
55
- push item unless include? item
56
- self
57
- end
58
- end
59
-
60
- refine String do
61
- def to_placeholder
62
- "#{PREFIX}#{self}#{SUFFIX}"
63
- end
64
- end
65
- end
66
-
67
5
  require_relative "drawght/parser"
6
+ require_relative "drawght/tracker"
68
7
  require_relative "drawght/compiler"
69
8
  require_relative "drawght/version"
70
9
 
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: drawght
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hallison Batista
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-03 00:00:00.000000000 Z
10
+ date: 2025-12-22 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
- Drawght v1.0.0 (2025-12-03)
13
+ Drawght v1.1.0 (2025-12-22)
14
14
 
15
15
  Drawght is a data handler for texts without logical statements. The goal is
16
16
  to use a dataset (such as the subject of a text) to draft a document
@@ -18,10 +18,9 @@ description: |
18
18
 
19
19
  Latest changes:
20
20
 
21
- - New compiler process.
22
- - Refined extension methods have been added.
23
- - Test coverage.
24
- - Syntax revision for nested values in collections.
21
+ - Syntax validation.
22
+ - Refined extension methods have been moved to their respective modules.
23
+ - Syntax for getting the length of a collection.
25
24
  email: email@hallison.dev.br
26
25
  executables: []
27
26
  extensions: []
@@ -34,6 +33,7 @@ files:
34
33
  - lib/drawght/compiler.rb
35
34
  - lib/drawght/mapper.rb
36
35
  - lib/drawght/parser.rb
36
+ - lib/drawght/tracker.rb
37
37
  - lib/drawght/version.rb
38
38
  homepage: https://drawght.github.io
39
39
  licenses:
@@ -55,5 +55,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
55
  requirements: []
56
56
  rubygems_version: 3.6.7
57
57
  specification_version: 4
58
- summary: Drawght parser implementation in Ruby.
58
+ summary: Drawght implementation in Ruby.
59
59
  test_files: []