toon-fu 4.1.3 → 4.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 +4 -4
- data/README.md +27 -8
- data/exe/toon +13 -0
- data/lib/toon_fu/decoder.rb +9 -5
- data/lib/toon_fu/field_list.rb +7 -2
- data/lib/toon_fu/header.rb +5 -5
- data/lib/toon_fu/lines.rb +6 -2
- data/lib/toon_fu/reader.rb +65 -78
- data/lib/toon_fu/string_literal.rb +4 -0
- data/lib/toon_fu/tokens.rb +8 -7
- data/lib/toon_fu/version.rb +1 -1
- data/lib/toon_fu/writer.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7f87e2144c7adb60f949127ea36ed9e26b84fb1ba5e4cd6893d9fc46f3e859e
|
|
4
|
+
data.tar.gz: 84c74750d67f681645a7cd0e84b46675919f3e0549cc4e168306510b1f8b45bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a78c3f872ff747c74af86f91a5e945b8dcf1b2eb5d10ea9694ad2a0e3efccc938fe7652a5cfda36713163f0f39d965dbe75f1e77698bdea3aea124e81bb1b194
|
|
7
|
+
data.tar.gz: 1b6fa5a87123368f9a7e92ee1fb1f91fce1950d583967acf007542b6ea10168c8b67c282cb87e96009841a039b657a23c77c11d4e47535d192bde41f9e82aaad
|
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 what the spec requires: declared lengths, row widths, duplicate keys, indentation. Pass `false` to read a document that bends those rules — a declared length is ignored, a duplicate key takes its last value.
|
|
141
|
+
- `strict:` — `true` (default) checks what the spec requires: declared lengths, row widths, duplicate keys, indentation. Pass `false` to read a document that bends those rules — a declared length is ignored, a duplicate key takes its last value, a stray over-indented line is skipped unless it is a bare token, which raises in either mode.
|
|
142
142
|
- `indent_size:` — spaces per nesting level, default `2`.
|
|
143
143
|
|
|
144
144
|
```ruby
|
|
@@ -147,19 +147,36 @@ decoder = ToonFu::Decoder.new(indent_size: 4) # reuse for many documents
|
|
|
147
147
|
decoder.decode(text)
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
## On the command line
|
|
151
|
+
|
|
152
|
+
The gem installs a `toon` executable that encodes JSON on stdin — handy for reading a command's output in fewer tokens:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
$ gh api repos/ruby/ruby/pulls --jq '.[0] | {number, title}' | toon
|
|
156
|
+
number: 13579
|
|
157
|
+
title: Fix a typo
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Input it cannot encode is passed through unchanged, so putting it at the end of a pipe never costs you the output:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
$ git log --oneline -1 | toon
|
|
164
|
+
b08ea2d feat: ship a toon executable
|
|
165
|
+
```
|
|
166
|
+
|
|
150
167
|
## Compared with other Ruby TOON gems
|
|
151
168
|
|
|
152
|
-
The only one that passes every spec fixture: toon-fu passes all
|
|
169
|
+
The only one that passes every spec fixture: toon-fu passes all 160 encode and all 385 decode fixtures. The table compares encoding — the 138 encode fixtures that use default options, which every gem can run; none of the others reads TOON back.
|
|
153
170
|
|
|
154
171
|
| Gem | Spec fixtures passed | Speed vs toon-fu |
|
|
155
172
|
|---|---:|---:|
|
|
156
|
-
| **toon-fu** | **
|
|
157
|
-
| sorbet-toon 0.1.0 |
|
|
158
|
-
| toon-ruby 0.1.1 |
|
|
159
|
-
| toon_my_json 0.1.0 |
|
|
160
|
-
| toon-format 0.1.2 |
|
|
173
|
+
| **toon-fu** | **138 / 138** | **1.00×** |
|
|
174
|
+
| sorbet-toon 0.1.0 | 104 / 138 | 0.63× |
|
|
175
|
+
| toon-ruby 0.1.1 | 100 / 138 | 0.62× |
|
|
176
|
+
| toon_my_json 0.1.0 | 48 / 138 | 1.62× |
|
|
177
|
+
| toon-format 0.1.2 | 40 / 138 | 1.31× |
|
|
161
178
|
|
|
162
|
-
The Ruby TOON encoders with more than 10,000 downloads, measured by [`benchmark/run.rb`](benchmark/run.rb) on Ruby 3.4.
|
|
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-07). **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.
|
|
163
180
|
|
|
164
181
|
What falls through the gaps:
|
|
165
182
|
|
|
@@ -186,6 +203,8 @@ The gem version tracks the TOON specification it implements:
|
|
|
186
203
|
- `MAJOR.MINOR` is the spec version: `X.Y.Z` speaks TOON `X.Y`.
|
|
187
204
|
- `PATCH` is the gem's own: fixes and improvements within the same dialect.
|
|
188
205
|
|
|
206
|
+
This release speaks `toon-spec: 4.3`.
|
|
207
|
+
|
|
189
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.
|
|
190
209
|
|
|
191
210
|
Release notes: [GitHub releases](https://github.com/hoblin/toon-fu/releases).
|
data/exe/toon
ADDED
data/lib/toon_fu/decoder.rb
CHANGED
|
@@ -5,8 +5,11 @@ module ToonFu
|
|
|
5
5
|
class Decoder
|
|
6
6
|
# @param strict [Boolean] whether to enforce the spec's strict-mode
|
|
7
7
|
# checks; with +false+ duplicate keys resolve last-write-wins, leading
|
|
8
|
-
# spaces that are not a multiple of indent_size are accepted,
|
|
9
|
-
#
|
|
8
|
+
# spaces that are not a multiple of indent_size are accepted, a tab in
|
|
9
|
+
# indentation is accepted and counts as one level, a line of spaces and
|
|
10
|
+
# tabs is blank, a scope's first line sets the scope's depth, an
|
|
11
|
+
# over-indented line is skipped unless it is a bare token, and a
|
|
12
|
+
# malformed or misplaced header is read as a key-value line
|
|
10
13
|
# @param indent_size [Integer] spaces per nesting level
|
|
11
14
|
# @raise [ArgumentError] when strict is not a boolean or indent_size is
|
|
12
15
|
# not a positive Integer
|
|
@@ -43,9 +46,10 @@ module ToonFu
|
|
|
43
46
|
# key without a colon; for a bare token line inside a scope; for
|
|
44
47
|
# content after a completed root form; for nesting beyond the depth
|
|
45
48
|
# limit; and, when strict, for a declared length or row width that
|
|
46
|
-
# does not match, a malformed header, a duplicate key, a
|
|
47
|
-
# inside a header's scope,
|
|
48
|
-
# indent_size
|
|
49
|
+
# does not match, a malformed or misplaced header, a duplicate key, a
|
|
50
|
+
# blank line inside a header's scope, a tab in indentation, indentation
|
|
51
|
+
# that is not a multiple of indent_size, a depth jump, and an
|
|
52
|
+
# over-indented line, an indented first line included
|
|
49
53
|
def decode(text)
|
|
50
54
|
Reader.new(@strict, @indent_size).read(utf8(text))
|
|
51
55
|
end
|
data/lib/toon_fu/field_list.rb
CHANGED
|
@@ -37,16 +37,21 @@ module ToonFu
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def split(entry)
|
|
40
|
+
entry = Tokens.trim(entry)
|
|
41
|
+
return [nil, nil] if entry.empty?
|
|
42
|
+
|
|
40
43
|
brace = Tokens.brace_index(entry)
|
|
41
|
-
return [
|
|
44
|
+
return [Token.key(entry), nil] if brace.nil?
|
|
42
45
|
|
|
43
46
|
name = entry[0...brace]
|
|
47
|
+
return [nil, nil] if name.empty? || name.match?(Tokens::TRAILING_WHITESPACE)
|
|
48
|
+
|
|
44
49
|
group = entry[brace..]
|
|
45
50
|
closing = Tokens.closing_brace(group)
|
|
46
51
|
return [nil, nil] unless closing == group.length - 1
|
|
47
52
|
|
|
48
53
|
nested = self.class.parse(group[1...closing], @delimiter)
|
|
49
|
-
nested.nil? ? [nil, nil] : [
|
|
54
|
+
nested.nil? ? [nil, nil] : [Token.key(name), nested]
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
data/lib/toon_fu/header.rb
CHANGED
|
@@ -9,7 +9,7 @@ module ToonFu
|
|
|
9
9
|
|
|
10
10
|
def self.parse(content)
|
|
11
11
|
bracket = Tokens.bracket_index(content)
|
|
12
|
-
return nil if bracket.nil?
|
|
12
|
+
return nil if bracket.nil? || Tokens.colon_index(content).nil?
|
|
13
13
|
|
|
14
14
|
new(content, bracket).parse
|
|
15
15
|
end
|
|
@@ -20,9 +20,9 @@ module ToonFu
|
|
|
20
20
|
@malformed = false
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# A line
|
|
24
|
-
# one that §6
|
|
25
|
-
#
|
|
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, a
|
|
25
|
+
# strict-mode error (§5.2).
|
|
26
26
|
def malformed?
|
|
27
27
|
@malformed
|
|
28
28
|
end
|
|
@@ -30,7 +30,7 @@ module ToonFu
|
|
|
30
30
|
def parse
|
|
31
31
|
@key = @bracket.zero? ? nil : @content[0...@bracket]
|
|
32
32
|
return nil unless @key.nil? || Tokens.whole_token?(@key)
|
|
33
|
-
return broken if @key&.match?(
|
|
33
|
+
return broken if @key&.match?(Tokens::TRAILING_WHITESPACE)
|
|
34
34
|
return broken unless (closing = @content.index("]", @bracket))
|
|
35
35
|
|
|
36
36
|
return broken unless segment(@content[(@bracket + 1)...closing])
|
data/lib/toon_fu/lines.rb
CHANGED
|
@@ -29,8 +29,8 @@ module ToonFu
|
|
|
29
29
|
BOM = "\u{feff}"
|
|
30
30
|
COMMENT = /\A *#/
|
|
31
31
|
TRAILING_SPACES = / +\z/
|
|
32
|
-
BLANK = /\A\s*\z/
|
|
33
32
|
INDENTATION = /\A[ \t]*/
|
|
33
|
+
BLANK_WITH_TABS = /\A[ \t]*\z/
|
|
34
34
|
|
|
35
35
|
def initialize(text, strict, indent_size)
|
|
36
36
|
@strict = strict
|
|
@@ -69,7 +69,7 @@ module ToonFu
|
|
|
69
69
|
line = raw.delete_suffix("\r").sub(TRAILING_SPACES, "")
|
|
70
70
|
next if COMMENT.match?(line)
|
|
71
71
|
|
|
72
|
-
if
|
|
72
|
+
if blank?(line)
|
|
73
73
|
blank = true
|
|
74
74
|
next
|
|
75
75
|
end
|
|
@@ -80,6 +80,10 @@ module ToonFu
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def blank?(line)
|
|
84
|
+
@strict ? line.empty? : BLANK_WITH_TABS.match?(line)
|
|
85
|
+
end
|
|
86
|
+
|
|
83
87
|
def build(line, number, blank)
|
|
84
88
|
Line.new(line.sub(INDENTATION, ""), depth(line, number), number, line, blank)
|
|
85
89
|
end
|
data/lib/toon_fu/reader.rb
CHANGED
|
@@ -8,6 +8,7 @@ module ToonFu
|
|
|
8
8
|
@strict = strict
|
|
9
9
|
@indent_size = indent_size
|
|
10
10
|
@depth = 0
|
|
11
|
+
@in_span = false
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def read(text)
|
|
@@ -21,18 +22,19 @@ module ToonFu
|
|
|
21
22
|
|
|
22
23
|
def root
|
|
23
24
|
line = @lines.peek
|
|
25
|
+
return object(-1, 0) unless line.depth.zero?
|
|
24
26
|
return empty_array_root if line.content == "[]"
|
|
25
27
|
|
|
26
28
|
header = usable(Header.parse(line.content), line)
|
|
27
29
|
return keyless_root(header) if header && header.key.nil?
|
|
28
|
-
return scalar_root(line) if @lines.one? &&
|
|
30
|
+
return scalar_root(line) if @lines.one? && scalar?(line)
|
|
29
31
|
|
|
30
|
-
object(0)
|
|
32
|
+
object(-1, 0)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def keyless_root(header)
|
|
34
36
|
@lines.next
|
|
35
|
-
value = header.fields? ? table(header,
|
|
37
|
+
value = header.fields? ? table(header, 0) : array(header, 0)
|
|
36
38
|
trailing
|
|
37
39
|
value
|
|
38
40
|
end
|
|
@@ -54,26 +56,30 @@ module ToonFu
|
|
|
54
56
|
raise Error, "cannot decode content after the root form: #{@lines.peek.text}"
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
def object(depth, result = {})
|
|
58
|
-
while (line = @lines.peek) && line.depth
|
|
59
|
-
if line.depth
|
|
59
|
+
def object(parent, depth, result = {})
|
|
60
|
+
while (line = @lines.peek) && line.depth > parent
|
|
61
|
+
if line.depth == depth
|
|
62
|
+
take
|
|
63
|
+
key, value = field(line, depth)
|
|
64
|
+
store(result, key, value, line)
|
|
65
|
+
else
|
|
60
66
|
orphan(line)
|
|
61
|
-
next
|
|
62
67
|
end
|
|
63
|
-
|
|
64
|
-
@lines.next
|
|
65
|
-
key, value = field(line, depth)
|
|
66
|
-
store(result, key, value, line)
|
|
67
68
|
end
|
|
68
69
|
result
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def orphan(line)
|
|
72
73
|
raise Error, "cannot decode a line that belongs to no scope on line #{line.number}: #{line.text}" if @strict
|
|
74
|
+
raise Error, "cannot decode a bare token line inside a scope on line #{line.number}: #{line.text}" if scalar?(line)
|
|
73
75
|
|
|
74
76
|
@lines.next
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
def scalar?(line)
|
|
80
|
+
Tokens.colon_index(line.content).nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
def store(result, key, value, line)
|
|
78
84
|
raise Error, "cannot decode a duplicate key on line #{line.number}: #{key.inspect}" if @strict && result.key?(key)
|
|
79
85
|
|
|
@@ -83,7 +89,7 @@ module ToonFu
|
|
|
83
89
|
def field(line, depth)
|
|
84
90
|
header = usable(Header.parse(line.content), line)
|
|
85
91
|
return [Tokens.key(header.key), header_value(header, depth)] if header&.key
|
|
86
|
-
|
|
92
|
+
misplaced(line, "a keyless array header in object field position") if header
|
|
87
93
|
|
|
88
94
|
colon = Tokens.colon_index(line.content)
|
|
89
95
|
raise Error, "cannot decode a line without a colon after its key on line #{line.number}: #{line.text}" if colon.nil?
|
|
@@ -99,30 +105,23 @@ module ToonFu
|
|
|
99
105
|
Token.decode(text)
|
|
100
106
|
end
|
|
101
107
|
|
|
102
|
-
def nested(
|
|
103
|
-
|
|
104
|
-
return {} if
|
|
108
|
+
def nested(parent)
|
|
109
|
+
depth = content_depth(parent)
|
|
110
|
+
return {} if depth.nil?
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
descend { object(depth + 1) }
|
|
112
|
+
descend { object(parent, depth) }
|
|
108
113
|
end
|
|
109
114
|
|
|
110
115
|
def header_value(header, depth)
|
|
111
|
-
|
|
112
|
-
return [] if header.length.zero? && !items?(depth + 1)
|
|
113
|
-
|
|
114
|
-
array(header, depth + 1)
|
|
116
|
+
header.fields? ? table(header, depth) : array(header, depth)
|
|
115
117
|
end
|
|
116
118
|
|
|
117
|
-
def
|
|
118
|
-
line = @lines.peek
|
|
119
|
-
!line.nil? && line.depth == depth
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def array(header, depth)
|
|
119
|
+
def array(header, parent)
|
|
123
120
|
return inline(header) unless header.inline.empty?
|
|
124
121
|
|
|
125
|
-
descend {
|
|
122
|
+
items = descend { gather(parent, stop: ->(line) { !line.item? }) { |line| item(line) } }
|
|
123
|
+
count(items.length, header, "item")
|
|
124
|
+
items
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
def inline(header)
|
|
@@ -131,46 +130,27 @@ module ToonFu
|
|
|
131
130
|
cells
|
|
132
131
|
end
|
|
133
132
|
|
|
134
|
-
def
|
|
135
|
-
items = []
|
|
136
|
-
while (line = @lines.peek) && line.depth == depth && line.item?
|
|
137
|
-
span(line) unless items.empty?
|
|
138
|
-
@lines.next
|
|
139
|
-
items << item(line, depth)
|
|
140
|
-
end
|
|
141
|
-
scalar_line(@lines.peek, depth)
|
|
142
|
-
count(items.length, header, "item")
|
|
143
|
-
items
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# A scalar line is valid only as a root primitive; inside a scope it is a
|
|
147
|
-
# structural error in strict and non-strict mode alike (§5.2, §14.2).
|
|
148
|
-
def scalar_line(line, depth)
|
|
149
|
-
return if line.nil? || line.depth != depth || Tokens.colon_index(line.content)
|
|
150
|
-
|
|
151
|
-
header = Header.parse(line.content)
|
|
152
|
-
return if header && !header.malformed?
|
|
153
|
-
|
|
154
|
-
raise Error, "cannot decode a bare token line inside a scope on line #{line.number}: #{line.text}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def item(line, depth)
|
|
133
|
+
def item(line)
|
|
158
134
|
rest = line.item_content
|
|
159
135
|
return {} if rest.empty?
|
|
160
136
|
return [] if rest == "[]"
|
|
161
137
|
|
|
162
138
|
header = usable(Header.parse(rest), line)
|
|
163
|
-
return item_header(header, line
|
|
164
|
-
return item_object(rest, depth) if Tokens.colon_index(rest)
|
|
139
|
+
return item_header(header, line) if header
|
|
140
|
+
return item_object(rest, line.depth) if Tokens.colon_index(rest)
|
|
165
141
|
|
|
166
142
|
Token.decode(Tokens.trim(rest))
|
|
167
143
|
end
|
|
168
144
|
|
|
169
|
-
def item_header(header, line
|
|
170
|
-
return
|
|
171
|
-
|
|
145
|
+
def item_header(header, line)
|
|
146
|
+
return array(header, line.depth) unless header.key || header.fields?
|
|
147
|
+
|
|
148
|
+
misplaced(line, "a keyless fields-bearing header as a list item") unless header.key
|
|
149
|
+
item_object(line.item_content, line.depth)
|
|
150
|
+
end
|
|
172
151
|
|
|
173
|
-
|
|
152
|
+
def misplaced(line, what)
|
|
153
|
+
raise Error, "cannot decode #{what} on line #{line.number}: #{line.text}" if @strict
|
|
174
154
|
end
|
|
175
155
|
|
|
176
156
|
def item_object(rest, depth)
|
|
@@ -178,20 +158,18 @@ module ToonFu
|
|
|
178
158
|
key, value = descend { field(synthetic, depth + 1) }
|
|
179
159
|
result = {}
|
|
180
160
|
store(result, key, value, synthetic)
|
|
181
|
-
|
|
182
|
-
span(following) if following && following.depth == depth + 1
|
|
183
|
-
descend { object(depth + 1, result) }
|
|
161
|
+
descend { object(depth, depth + 1, result) }
|
|
184
162
|
end
|
|
185
163
|
|
|
186
|
-
def table(header,
|
|
164
|
+
def table(header, parent)
|
|
187
165
|
raise Error, "cannot decode a field name repeated in one field list: #{header.duplicate.inspect}" if @strict && header.duplicate
|
|
188
166
|
|
|
189
|
-
header.keyed? ? entries(header,
|
|
167
|
+
header.keyed? ? entries(header, parent) : rows(header, parent)
|
|
190
168
|
end
|
|
191
169
|
|
|
192
|
-
def rows(header,
|
|
170
|
+
def rows(header, parent)
|
|
193
171
|
collected = descend do
|
|
194
|
-
gather(
|
|
172
|
+
gather(parent, stop: ->(line) { key_value_line?(line, header.delimiter) }) { |line| row(header, line) }
|
|
195
173
|
end
|
|
196
174
|
count(collected.length, header, "row")
|
|
197
175
|
collected
|
|
@@ -207,10 +185,10 @@ module ToonFu
|
|
|
207
185
|
cell.nil? || colon < cell
|
|
208
186
|
end
|
|
209
187
|
|
|
210
|
-
def entries(header,
|
|
188
|
+
def entries(header, parent)
|
|
211
189
|
result = {}
|
|
212
190
|
collected = descend do
|
|
213
|
-
gather(
|
|
191
|
+
gather(parent) do |line|
|
|
214
192
|
colon = Tokens.colon_index(line.content)
|
|
215
193
|
if colon.nil?
|
|
216
194
|
raise Error, "cannot decode an entry row without a colon on line #{line.number}: #{line.text}" if @strict
|
|
@@ -226,22 +204,29 @@ module ToonFu
|
|
|
226
204
|
result
|
|
227
205
|
end
|
|
228
206
|
|
|
229
|
-
def gather(
|
|
207
|
+
def gather(parent, stop: nil)
|
|
208
|
+
outer = @in_span
|
|
230
209
|
collected = []
|
|
231
|
-
|
|
210
|
+
depth = content_depth(parent)
|
|
211
|
+
while depth && (line = @lines.peek) && line.depth > parent
|
|
212
|
+
if line.depth != depth
|
|
213
|
+
orphan(line)
|
|
214
|
+
next
|
|
215
|
+
end
|
|
232
216
|
break if stop&.call(line)
|
|
233
217
|
|
|
234
|
-
|
|
235
|
-
@
|
|
218
|
+
take
|
|
219
|
+
@in_span = true
|
|
236
220
|
collected << yield(line)
|
|
237
221
|
end
|
|
238
222
|
collected
|
|
223
|
+
ensure
|
|
224
|
+
@in_span = outer
|
|
239
225
|
end
|
|
240
226
|
|
|
241
|
-
def
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
raise Error, "cannot decode a blank line inside a header span, before line #{line.number}: #{line.text}"
|
|
227
|
+
def take
|
|
228
|
+
line = @lines.next
|
|
229
|
+
raise Error, "cannot decode a blank line inside a header span, before line #{line.number}: #{line.text}" if @strict && @in_span && line.after_blank
|
|
245
230
|
end
|
|
246
231
|
|
|
247
232
|
def row(header, line, text = line.content)
|
|
@@ -285,10 +270,12 @@ module ToonFu
|
|
|
285
270
|
nil
|
|
286
271
|
end
|
|
287
272
|
|
|
288
|
-
def
|
|
289
|
-
|
|
273
|
+
def content_depth(parent)
|
|
274
|
+
line = @lines.peek
|
|
275
|
+
return if line.nil? || line.depth <= parent
|
|
276
|
+
raise Error, "cannot decode a depth jump on line #{line.number}: #{line.text}" if @strict && line.depth > parent + 1
|
|
290
277
|
|
|
291
|
-
|
|
278
|
+
line.depth
|
|
292
279
|
end
|
|
293
280
|
|
|
294
281
|
def descend
|
data/lib/toon_fu/tokens.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module ToonFu
|
|
4
4
|
module Tokens
|
|
5
|
-
|
|
5
|
+
QUOTE_SPAN = /"(?:[^"\\]|\\.?)*"?/
|
|
6
|
+
TRAILING_WHITESPACE = /[[:space:]]\z/
|
|
6
7
|
UNTIL_COLON = /[^":]+/
|
|
7
8
|
UNTIL_BRACKET = /[^":\[]+/
|
|
8
9
|
UNTIL_BRACE = /[^"{]+/
|
|
@@ -17,7 +18,7 @@ module ToonFu
|
|
|
17
18
|
until scanner.eos?
|
|
18
19
|
next if scanner.skip(pattern)
|
|
19
20
|
return scanner.charpos if stops.include?(scanner.peek(1))
|
|
20
|
-
|
|
21
|
+
scanner.skip(QUOTE_SPAN)
|
|
21
22
|
end
|
|
22
23
|
nil
|
|
23
24
|
end
|
|
@@ -40,7 +41,7 @@ module ToonFu
|
|
|
40
41
|
depth = 0
|
|
41
42
|
scanner = StringScanner.new(text)
|
|
42
43
|
until scanner.eos?
|
|
43
|
-
if scanner.skip(
|
|
44
|
+
if scanner.skip(QUOTE_SPAN)
|
|
44
45
|
next
|
|
45
46
|
elsif scanner.skip(/\{/)
|
|
46
47
|
depth += 1
|
|
@@ -48,7 +49,7 @@ module ToonFu
|
|
|
48
49
|
depth -= 1
|
|
49
50
|
return scanner.charpos - 1 if depth.zero?
|
|
50
51
|
else
|
|
51
|
-
scanner.skip(/[^"{}]+/)
|
|
52
|
+
scanner.skip(/[^"{}]+/)
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
nil
|
|
@@ -60,7 +61,7 @@ module ToonFu
|
|
|
60
61
|
depth = 0
|
|
61
62
|
scanner = StringScanner.new(text)
|
|
62
63
|
until scanner.eos?
|
|
63
|
-
if (quoted = scanner.scan(
|
|
64
|
+
if (quoted = scanner.scan(QUOTE_SPAN))
|
|
64
65
|
current << quoted
|
|
65
66
|
elsif depth.zero? && scanner.skip(delimiter)
|
|
66
67
|
entries << current
|
|
@@ -81,13 +82,13 @@ module ToonFu
|
|
|
81
82
|
current = +""
|
|
82
83
|
scanner = StringScanner.new(text)
|
|
83
84
|
until scanner.eos?
|
|
84
|
-
if (quoted = scanner.scan(
|
|
85
|
+
if (quoted = scanner.scan(QUOTE_SPAN))
|
|
85
86
|
current << quoted
|
|
86
87
|
elsif scanner.skip(delimiter)
|
|
87
88
|
cells << current
|
|
88
89
|
current = +""
|
|
89
90
|
else
|
|
90
|
-
current <<
|
|
91
|
+
current << scanner.scan(UNTIL_CELL_END.fetch(delimiter))
|
|
91
92
|
end
|
|
92
93
|
end
|
|
93
94
|
cells << current
|
data/lib/toon_fu/version.rb
CHANGED
data/lib/toon_fu/writer.rb
CHANGED
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toon-fu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yevhenii Hurin
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
@@ -16,12 +16,14 @@ description: 'Encodes Ruby hashes, arrays, dates and your own objects into TOON,
|
|
|
16
16
|
an error so your data arrives complete.'
|
|
17
17
|
email:
|
|
18
18
|
- evgeny.gurin@gmail.com
|
|
19
|
-
executables:
|
|
19
|
+
executables:
|
|
20
|
+
- toon
|
|
20
21
|
extensions: []
|
|
21
22
|
extra_rdoc_files: []
|
|
22
23
|
files:
|
|
23
24
|
- LICENSE
|
|
24
25
|
- README.md
|
|
26
|
+
- exe/toon
|
|
25
27
|
- lib/toon-fu.rb
|
|
26
28
|
- lib/toon_fu.rb
|
|
27
29
|
- lib/toon_fu/decimal_literal.rb
|