toon-fu 4.3.0 → 4.4.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/README.md +8 -8
- data/lib/toon_fu/decoder.rb +16 -14
- data/lib/toon_fu/header.rb +3 -3
- data/lib/toon_fu/lines.rb +1 -7
- data/lib/toon_fu/reader.rb +22 -40
- data/lib/toon_fu/tokens.rb +1 -1
- data/lib/toon_fu/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: fe7b6913ab8e4bd4224ed1b08175c8ae7b2e2a5b841c990daa77f48819f4f495
|
|
4
|
+
data.tar.gz: 536f77da3c7fe3ce3a34c0d7421d5777559a8a912ab6194582be07bff441e0e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be1b854a3300c9e7b70cd2fa73d2d9d6ace099c88c32d1f98a3d30152a2f2c03ab42de0f23b48bbacb6eccfa042946add466eb99dec2c7e3636cb0089857e0f8
|
|
7
|
+
data.tar.gz: 40cad3826f9fd815fd636fb4ac2c6b3b4e7563fefa73a9cbbfe1444161590222f914557ecaaf9d142a680b6a741cea300579e82cf5a711a943044f7adb614a83
|
data/README.md
CHANGED
|
@@ -138,7 +138,7 @@ ToonFu.decode("tags[3]: a,b")
|
|
|
138
138
|
|
|
139
139
|
### Options
|
|
140
140
|
|
|
141
|
-
- `strict:` — `true` (default) checks
|
|
141
|
+
- `strict:` — `true` (default) checks everything the spec requires. Pass `false` for the spec's five recoveries and nothing more: a declared length is ignored, a duplicate key takes its last value, a tab indents one level, a blank line between an array's items or rows is skipped, and a scope's first line sets its depth. Everything else — a malformed header, a row of the wrong width, a stray over-indented line — raises in either mode.
|
|
142
142
|
- `indent_size:` — spaces per nesting level, default `2`.
|
|
143
143
|
|
|
144
144
|
```ruby
|
|
@@ -166,17 +166,17 @@ b08ea2d feat: ship a toon executable
|
|
|
166
166
|
|
|
167
167
|
## Compared with other Ruby TOON gems
|
|
168
168
|
|
|
169
|
-
The only one that passes every spec fixture: toon-fu passes all 160 encode and all
|
|
169
|
+
The only one that passes every spec fixture: toon-fu passes all 160 encode and all 404 decode fixtures. The table compares encoding — the 138 encode fixtures that use default options; the other 22 set a delimiter or an indent size, which the other gems do not take, and none of them reads TOON back.
|
|
170
170
|
|
|
171
171
|
| Gem | Spec fixtures passed | Speed vs toon-fu |
|
|
172
172
|
|---|---:|---:|
|
|
173
173
|
| **toon-fu** | **138 / 138** | **1.00×** |
|
|
174
|
-
| sorbet-toon 0.1.0 | 104 / 138 | 0.
|
|
175
|
-
| toon-ruby 0.1.1 | 100 / 138 | 0.
|
|
176
|
-
| toon_my_json 0.1.0 |
|
|
177
|
-
| toon-format 0.1.2 | 40 / 138 | 1.
|
|
174
|
+
| sorbet-toon 0.1.0 | 104 / 138 | 0.55× |
|
|
175
|
+
| toon-ruby 0.1.1 | 100 / 138 | 0.55× |
|
|
176
|
+
| toon_my_json 0.1.0 | 46 / 138 | 1.49× |
|
|
177
|
+
| toon-format 0.1.2 | 40 / 138 | 1.11× |
|
|
178
178
|
|
|
179
|
-
The Ruby TOON encoders with more than 10,000 downloads, measured by [`benchmark/run.rb`](benchmark/run.rb) on Ruby 3.4.11 (2026-10-
|
|
179
|
+
The Ruby TOON encoders with more than 10,000 downloads, measured by [`benchmark/run.rb`](benchmark/run.rb) on Ruby 3.4.11 (2026-10-10). **Spec fixtures** are the spec's own encode fixtures that use default options. **Speed** is the geometric mean of encodes per second over five workloads — tables of 100 and 1000 rows, nested objects, a list of mixed objects, strings needing quotes — relative to toon-fu.
|
|
180
180
|
|
|
181
181
|
What falls through the gaps:
|
|
182
182
|
|
|
@@ -203,7 +203,7 @@ The gem version tracks the TOON specification it implements:
|
|
|
203
203
|
- `MAJOR.MINOR` is the spec version: `X.Y.Z` speaks TOON `X.Y`.
|
|
204
204
|
- `PATCH` is the gem's own: fixes and improvements within the same dialect.
|
|
205
205
|
|
|
206
|
-
This release speaks `toon-spec: 4.
|
|
206
|
+
This release speaks `toon-spec: 4.4`.
|
|
207
207
|
|
|
208
208
|
Pin the spec line with `gem "toon-fu", "~> X.Y.0"`: it takes our fixes and keeps you on the dialect you speak. The gem badge above shows the current release; the spec-drift badge turns red when a newer spec is released and toon-fu has not caught up yet.
|
|
209
209
|
|
data/lib/toon_fu/decoder.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module ToonFu
|
|
4
4
|
# Decodes TOON with one set of options, for callers that decode many.
|
|
5
5
|
class Decoder
|
|
6
|
-
# @param strict [Boolean] whether to enforce the spec
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
6
|
+
# @param strict [Boolean] whether to enforce every check of the spec;
|
|
7
|
+
# with +false+ exactly the spec's five recoveries apply: a declared
|
|
8
|
+
# length is ignored; a duplicate key or a field name repeated in one
|
|
9
|
+
# field list resolves last-write-wins, keeping its first position; a
|
|
10
|
+
# tab in indentation counts as one level, leading spaces need not be a
|
|
11
|
+
# multiple of indent_size and a line of spaces and tabs is blank; a
|
|
12
|
+
# blank line inside a header span is ignored; and a scope's first line
|
|
13
|
+
# sets the scope's depth
|
|
13
14
|
# @param indent_size [Integer] spaces per nesting level
|
|
14
15
|
# @raise [ArgumentError] when strict is not a boolean or indent_size is
|
|
15
16
|
# not a positive Integer
|
|
@@ -43,13 +44,14 @@ module ToonFu
|
|
|
43
44
|
# @raise [Error] for text that is not a String or not valid UTF-8; for a
|
|
44
45
|
# number outside the Float range; for an invalid escape, an
|
|
45
46
|
# unterminated quoted string, or content after a closing quote; for a
|
|
46
|
-
# key without a colon; for a
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
47
|
+
# key without a colon; for a malformed or misplaced header; for a row
|
|
48
|
+
# width that does not match; for a line that belongs to no scope, an
|
|
49
|
+
# over-indented or indented first line included; for content after a
|
|
50
|
+
# completed root form; for nesting beyond the depth limit; and, when
|
|
51
|
+
# strict, for a declared length that does not match, a duplicate key, a
|
|
52
|
+
# field name repeated in one field list, a blank line inside a header
|
|
53
|
+
# span, a tab in indentation, indentation that is not a multiple of
|
|
54
|
+
# indent_size, and a depth jump
|
|
53
55
|
def decode(text)
|
|
54
56
|
Reader.new(@strict, @indent_size).read(utf8(text))
|
|
55
57
|
end
|
data/lib/toon_fu/header.rb
CHANGED
|
@@ -21,8 +21,8 @@ module ToonFu
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# A line whose first unquoted "[" precedes its first unquoted colon is an
|
|
24
|
-
# array-header line; one that fails the §6 grammar is malformed,
|
|
25
|
-
#
|
|
24
|
+
# array-header line; one that fails the §6 grammar is malformed, an
|
|
25
|
+
# error in either mode (§5.2, §14.2).
|
|
26
26
|
def malformed?
|
|
27
27
|
@malformed
|
|
28
28
|
end
|
|
@@ -38,7 +38,7 @@ module ToonFu
|
|
|
38
38
|
# §6: a keyed header's field list is required, and a fields-bearing
|
|
39
39
|
# header carries no inline content.
|
|
40
40
|
return broken if @keyed && !fields?
|
|
41
|
-
return broken if fields? &&
|
|
41
|
+
return broken if fields? && !Tokens.trim(@inline).empty?
|
|
42
42
|
|
|
43
43
|
self
|
|
44
44
|
end
|
data/lib/toon_fu/lines.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module ToonFu
|
|
4
4
|
class Line
|
|
5
|
-
ITEM = /\A-(?:
|
|
5
|
+
ITEM = /\A-(?: +|\z)/
|
|
6
6
|
|
|
7
7
|
attr_reader :content, :depth, :number, :text
|
|
8
8
|
|
|
@@ -57,10 +57,6 @@ module ToonFu
|
|
|
57
57
|
line
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def number
|
|
61
|
-
peek ? peek.number : @lines.length
|
|
62
|
-
end
|
|
63
|
-
|
|
64
60
|
private
|
|
65
61
|
|
|
66
62
|
def significant(text)
|
|
@@ -88,8 +84,6 @@ module ToonFu
|
|
|
88
84
|
Line.new(line.sub(INDENTATION, ""), depth(line, number), number, line, blank)
|
|
89
85
|
end
|
|
90
86
|
|
|
91
|
-
# Non-strict mode accepts a tab as indentation (§12), where each tab
|
|
92
|
-
# counts as one level; strict mode rejects it.
|
|
93
87
|
def depth(line, number)
|
|
94
88
|
indent = line[INDENTATION]
|
|
95
89
|
raise Error, "cannot decode a line indented with a tab on line #{number}: #{line}" if @strict && indent.include?("\t")
|
data/lib/toon_fu/reader.rb
CHANGED
|
@@ -25,7 +25,7 @@ module ToonFu
|
|
|
25
25
|
return object(-1, 0) unless line.depth.zero?
|
|
26
26
|
return empty_array_root if line.content == "[]"
|
|
27
27
|
|
|
28
|
-
header =
|
|
28
|
+
header = parse_header(line)
|
|
29
29
|
return keyless_root(header) if header && header.key.nil?
|
|
30
30
|
return scalar_root(line) if @lines.one? && scalar?(line)
|
|
31
31
|
|
|
@@ -70,10 +70,7 @@ module ToonFu
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def orphan(line)
|
|
73
|
-
raise Error, "cannot decode a line that belongs to no scope on line #{line.number}: #{line.text}"
|
|
74
|
-
raise Error, "cannot decode a bare token line inside a scope on line #{line.number}: #{line.text}" if scalar?(line)
|
|
75
|
-
|
|
76
|
-
@lines.next
|
|
73
|
+
raise Error, "cannot decode a line that belongs to no scope on line #{line.number}: #{line.text}"
|
|
77
74
|
end
|
|
78
75
|
|
|
79
76
|
def scalar?(line)
|
|
@@ -87,12 +84,12 @@ module ToonFu
|
|
|
87
84
|
end
|
|
88
85
|
|
|
89
86
|
def field(line, depth)
|
|
90
|
-
header =
|
|
87
|
+
header = parse_header(line)
|
|
91
88
|
return [Tokens.key(header.key), header_value(header, depth)] if header&.key
|
|
92
89
|
misplaced(line, "a keyless array header in object field position") if header
|
|
93
90
|
|
|
94
91
|
colon = Tokens.colon_index(line.content)
|
|
95
|
-
raise Error, "cannot decode a line without a colon
|
|
92
|
+
raise Error, "cannot decode a line without a colon on line #{line.number}: #{line.text}" if colon.nil?
|
|
96
93
|
|
|
97
94
|
key = Tokens.key(line.content[0...colon])
|
|
98
95
|
[key, field_value(Tokens.trim(line.content[(colon + 1)..]), depth)]
|
|
@@ -135,9 +132,9 @@ module ToonFu
|
|
|
135
132
|
return {} if rest.empty?
|
|
136
133
|
return [] if rest == "[]"
|
|
137
134
|
|
|
138
|
-
header =
|
|
135
|
+
header = parse_header(line, rest)
|
|
139
136
|
return item_header(header, line) if header
|
|
140
|
-
return item_object(rest, line
|
|
137
|
+
return item_object(rest, line) if Tokens.colon_index(rest)
|
|
141
138
|
|
|
142
139
|
Token.decode(Tokens.trim(rest))
|
|
143
140
|
end
|
|
@@ -146,19 +143,20 @@ module ToonFu
|
|
|
146
143
|
return array(header, line.depth) unless header.key || header.fields?
|
|
147
144
|
|
|
148
145
|
misplaced(line, "a keyless fields-bearing header as a list item") unless header.key
|
|
149
|
-
item_object(line.item_content, line
|
|
146
|
+
item_object(line.item_content, line)
|
|
150
147
|
end
|
|
151
148
|
|
|
152
149
|
def misplaced(line, what)
|
|
153
|
-
raise Error, "cannot decode #{what} on line #{line.number}: #{line.text}"
|
|
150
|
+
raise Error, "cannot decode #{what} on line #{line.number}: #{line.text}"
|
|
154
151
|
end
|
|
155
152
|
|
|
156
|
-
def item_object(rest,
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
def item_object(rest, line)
|
|
154
|
+
depth = line.depth + 1
|
|
155
|
+
synthetic = Line.new(rest, depth, line.number)
|
|
156
|
+
key, value = descend { field(synthetic, depth) }
|
|
159
157
|
result = {}
|
|
160
158
|
store(result, key, value, synthetic)
|
|
161
|
-
descend { object(depth, depth
|
|
159
|
+
descend { object(line.depth, depth, result) }
|
|
162
160
|
end
|
|
163
161
|
|
|
164
162
|
def table(header, parent)
|
|
@@ -190,15 +188,11 @@ module ToonFu
|
|
|
190
188
|
collected = descend do
|
|
191
189
|
gather(parent) do |line|
|
|
192
190
|
colon = Tokens.colon_index(line.content)
|
|
193
|
-
if colon.nil?
|
|
194
|
-
raise Error, "cannot decode an entry row without a colon on line #{line.number}: #{line.text}" if @strict
|
|
191
|
+
raise Error, "cannot decode an entry row without a colon on line #{line.number}: #{line.text}" if colon.nil?
|
|
195
192
|
|
|
196
|
-
next nil
|
|
197
|
-
end
|
|
198
193
|
[Tokens.key(line.content[0...colon]), row(header, line, line.content[(colon + 1)..]), line]
|
|
199
194
|
end
|
|
200
195
|
end
|
|
201
|
-
collected.compact!
|
|
202
196
|
count(collected.length, header, "entry row")
|
|
203
197
|
collected.each { |key, value, line| store(result, key, value, line) }
|
|
204
198
|
result
|
|
@@ -209,10 +203,7 @@ module ToonFu
|
|
|
209
203
|
collected = []
|
|
210
204
|
depth = content_depth(parent)
|
|
211
205
|
while depth && (line = @lines.peek) && line.depth > parent
|
|
212
|
-
if line.depth != depth
|
|
213
|
-
orphan(line)
|
|
214
|
-
next
|
|
215
|
-
end
|
|
206
|
+
orphan(line) if line.depth != depth
|
|
216
207
|
break if stop&.call(line)
|
|
217
208
|
|
|
218
209
|
take
|
|
@@ -233,21 +224,12 @@ module ToonFu
|
|
|
233
224
|
cells = Tokens.split(text, header.delimiter)
|
|
234
225
|
cells = [] if cells.length == 1 && Tokens.trim(cells.first).empty?
|
|
235
226
|
width(cells.length, header, line)
|
|
236
|
-
shape(header.fields, cells
|
|
227
|
+
shape(header.fields, cells)
|
|
237
228
|
end
|
|
238
229
|
|
|
239
230
|
def shape(fields, cells)
|
|
240
231
|
fields.each_with_object({}) do |(name, nested), result|
|
|
241
|
-
|
|
242
|
-
result[name] = shape(nested, cells)
|
|
243
|
-
else
|
|
244
|
-
cell = begin
|
|
245
|
-
cells.next
|
|
246
|
-
rescue StopIteration
|
|
247
|
-
next
|
|
248
|
-
end
|
|
249
|
-
result[name] = Token.decode(Tokens.trim(cell))
|
|
250
|
-
end
|
|
232
|
+
result[name] = nested ? shape(nested, cells) : Token.decode(Tokens.trim(cells.shift))
|
|
251
233
|
end
|
|
252
234
|
end
|
|
253
235
|
|
|
@@ -258,16 +240,16 @@ module ToonFu
|
|
|
258
240
|
end
|
|
259
241
|
|
|
260
242
|
def width(actual, header, line)
|
|
261
|
-
return
|
|
243
|
+
return if actual == header.leaves.length
|
|
262
244
|
|
|
263
245
|
raise Error, "cannot decode #{actual} cells on line #{line.number} where the header declares #{header.leaves.length} field#{"s" unless header.leaves.length == 1}"
|
|
264
246
|
end
|
|
265
247
|
|
|
266
|
-
def
|
|
267
|
-
|
|
268
|
-
raise Error, "cannot decode a malformed array header on line #{line.number}: #{line.text}" if
|
|
248
|
+
def parse_header(line, content = line.content)
|
|
249
|
+
header = Header.parse(content)
|
|
250
|
+
raise Error, "cannot decode a malformed array header on line #{line.number}: #{line.text}" if header&.malformed?
|
|
269
251
|
|
|
270
|
-
|
|
252
|
+
header
|
|
271
253
|
end
|
|
272
254
|
|
|
273
255
|
def content_depth(parent)
|
data/lib/toon_fu/tokens.rb
CHANGED
data/lib/toon_fu/version.rb
CHANGED