nestedtext 3.2.1 → 4.0.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/CHANGELOG.md +10 -5
- data/README.md +16 -13
- data/lib/nestedtext/constants.rb +6 -3
- data/lib/nestedtext/core_ext.rb +7 -12
- data/lib/nestedtext/core_ext_internal.rb +3 -1
- data/lib/nestedtext/decode.rb +11 -9
- data/lib/nestedtext/dumper.rb +74 -69
- data/lib/nestedtext/encode.rb +7 -6
- data/lib/nestedtext/encode_helpers.rb +4 -3
- data/lib/nestedtext/error.rb +2 -0
- data/lib/nestedtext/errors_internal.rb +82 -58
- data/lib/nestedtext/inline_parser.rb +134 -0
- data/lib/nestedtext/parser.rb +144 -185
- data/lib/nestedtext/scanners.rb +21 -15
- data/lib/nestedtext/version.rb +1 -1
- data/lib/nestedtext.rb +6 -6
- metadata +3 -2
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'word_wrap'
|
4
|
+
require 'word_wrap/core_ext'
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require 'nestedtext/constants'
|
7
|
+
require 'nestedtext/error'
|
8
8
|
|
9
9
|
module NestedText
|
10
10
|
module Errors
|
11
11
|
class InternalError < Error
|
12
|
-
public_class_method :new # Prevent users from
|
12
|
+
public_class_method :new # Prevent users from instantiating.
|
13
13
|
end
|
14
14
|
|
15
15
|
class ParseError < InternalError
|
@@ -26,26 +26,44 @@ module NestedText
|
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
def lineno_disp
|
30
|
+
@lineno + 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def colno_disp
|
34
|
+
@colno + 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# Number of digits in the line number.
|
38
|
+
def lineno_digits
|
39
|
+
lineno_disp.to_s.length
|
40
|
+
end
|
41
|
+
|
42
|
+
def pretty_prefix
|
43
|
+
"\nParse ParseError (line #{lineno_disp}, column #{colno_disp}): "
|
44
|
+
end
|
45
|
+
|
46
|
+
def pretty_line(line)
|
47
|
+
return '' if line.nil?
|
48
|
+
|
49
|
+
lline_indent = ' ' * line.indentation
|
50
|
+
prev_lineno_disp = line.lineno + 1
|
33
51
|
|
34
|
-
last_lines = ""
|
35
52
|
# From one line to another, we can at most have 1 digits length difference.
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
line_indent = " " * line.indentation
|
43
|
-
last_lines += "\n\t#{lineno_disp}│#{line_indent}#{line.content}"
|
53
|
+
"\n\t#{prev_lineno_disp.to_s.rjust(lineno_digits)}│#{lline_indent}#{line.content}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def pretty_last_lines(line)
|
57
|
+
pretty_line(line.prev) + pretty_line(line)
|
58
|
+
end
|
44
59
|
|
45
|
-
|
46
|
-
|
60
|
+
def pretty_marker
|
61
|
+
marker_indent = colno_disp + lineno_digits # +1 for the "|"
|
62
|
+
"\n\t#{' ' * marker_indent}^"
|
63
|
+
end
|
47
64
|
|
48
|
-
|
65
|
+
def pretty_message(line)
|
66
|
+
pretty_prefix + @message_raw + pretty_last_lines(line) + pretty_marker
|
49
67
|
end
|
50
68
|
end
|
51
69
|
|
@@ -57,19 +75,19 @@ module NestedText
|
|
57
75
|
|
58
76
|
class ParseLineTagNotDetectedError < ParseError
|
59
77
|
def initialize(line)
|
60
|
-
super(line, line.indentation,
|
78
|
+
super(line, line.indentation, 'unrecognized line.')
|
61
79
|
end
|
62
80
|
end
|
63
81
|
|
64
82
|
class ParseLineTypeExpectedListItemError < ParseError
|
65
83
|
def initialize(line)
|
66
|
-
super(line, line.indentation,
|
84
|
+
super(line, line.indentation, 'expected list item.')
|
67
85
|
end
|
68
86
|
end
|
69
87
|
|
70
88
|
class ParseMultilineKeyNoValueError < ParseError
|
71
89
|
def initialize(line)
|
72
|
-
super(line, line.indentation,
|
90
|
+
super(line, line.indentation, 'multiline key requires a value.')
|
73
91
|
end
|
74
92
|
end
|
75
93
|
|
@@ -87,7 +105,7 @@ module NestedText
|
|
87
105
|
|
88
106
|
class ParseInlineMissingValueError < ParseError
|
89
107
|
def initialize(line, colno)
|
90
|
-
super(line, line.indentation + colno,
|
108
|
+
super(line, line.indentation + colno, 'expected value.')
|
91
109
|
end
|
92
110
|
end
|
93
111
|
|
@@ -99,13 +117,13 @@ module NestedText
|
|
99
117
|
|
100
118
|
class ParseInlineNoClosingDelimiterError < ParseError
|
101
119
|
def initialize(line, colno)
|
102
|
-
super(line, line.indentation + colno,
|
120
|
+
super(line, line.indentation + colno, 'line ended without closing delimiter.')
|
103
121
|
end
|
104
122
|
end
|
105
123
|
|
106
124
|
class ParseInlineExtraCharactersAfterDelimiterError < ParseError
|
107
125
|
def initialize(line, colno, extra_chars)
|
108
|
-
character_str = extra_chars.length > 1 ?
|
126
|
+
character_str = extra_chars.length > 1 ? 'characters' : 'character'
|
109
127
|
super(line, line.indentation + colno, "extra #{character_str} after closing delimiter: ‘#{extra_chars}’.")
|
110
128
|
end
|
111
129
|
end
|
@@ -113,19 +131,15 @@ module NestedText
|
|
113
131
|
class ParseInvalidIndentationError < ParseError
|
114
132
|
def initialize(line, ind_exp)
|
115
133
|
prev_line = line.prev
|
116
|
-
if prev_line.nil? && ind_exp
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
message = "invalid indentation, partial dedent."
|
126
|
-
else
|
127
|
-
message = "invalid indentation."
|
128
|
-
end
|
134
|
+
message = if prev_line.nil? && ind_exp.zero?
|
135
|
+
'top-level content must start in column 1.'
|
136
|
+
elsif !prev_line.nil? && line.indentation < prev_line.indentation
|
137
|
+
# Can't use ind_exp here, because it's a difference if the previous line was further indented.
|
138
|
+
# See test_load_error_dict_10
|
139
|
+
'invalid indentation, partial dedent.'
|
140
|
+
else
|
141
|
+
'invalid indentation.'
|
142
|
+
end
|
129
143
|
# Need to wrap like official tests. #wrap always add an extra \n we need to chop off.
|
130
144
|
message_wrapped = message.wrap(70).chop
|
131
145
|
super(line, ind_exp, message_wrapped)
|
@@ -134,25 +148,27 @@ module NestedText
|
|
134
148
|
|
135
149
|
class ParseLineTypeNotExpectedError < ParseError
|
136
150
|
def initialize(line, type_exps, type_act)
|
137
|
-
super(line, line.indentation,
|
151
|
+
super(line, line.indentation,
|
152
|
+
"The current line was detected to be #{type_act}, "\
|
153
|
+
"but we expected to see any of [#{type_exps.join(', ')}] here.")
|
138
154
|
end
|
139
155
|
end
|
140
156
|
|
141
157
|
class ParseLineTypeExpectedDictItemError < ParseError
|
142
158
|
def initialize(line)
|
143
|
-
super(line, line.indentation,
|
159
|
+
super(line, line.indentation, 'expected dictionary item.')
|
144
160
|
end
|
145
161
|
end
|
146
162
|
|
147
163
|
class ParseInvalidIndentationCharError < ParseError
|
148
164
|
def initialize(line)
|
149
|
-
printable_char = line.content[0].dump.gsub(/"/,
|
165
|
+
printable_char = line.content[0].dump.gsub(/"/, '')
|
150
166
|
|
151
|
-
# Looking for non-breaking space is just to be
|
152
|
-
explanation =
|
167
|
+
# Looking for non-breaking space is just to be compatible with official tests.
|
168
|
+
explanation = ''
|
153
169
|
if printable_char == '\\u00A0'
|
154
170
|
printable_char = '\\xa0'
|
155
|
-
explanation =
|
171
|
+
explanation = ' (NO-BREAK SPACE)'
|
156
172
|
end
|
157
173
|
|
158
174
|
message = "invalid character in indentation: '#{printable_char}'#{explanation}."
|
@@ -162,19 +178,23 @@ module NestedText
|
|
162
178
|
|
163
179
|
class ParseDictDuplicateKeyError < ParseError
|
164
180
|
def initialize(line)
|
165
|
-
super(line, line.indentation, "duplicate key: #{line.attribs[
|
181
|
+
super(line, line.indentation, "duplicate key: #{line.attribs['key']}.")
|
166
182
|
end
|
167
183
|
end
|
168
184
|
|
169
185
|
class ParseCustomClassNotFoundError < ParseError
|
170
186
|
def initialize(line, class_name)
|
171
|
-
super(line, line.indentation,
|
187
|
+
super(line, line.indentation,
|
188
|
+
"Detected an encode custom class #{class_name} " \
|
189
|
+
"however we can't find it, so it can't be deserialzied.")
|
172
190
|
end
|
173
191
|
end
|
174
192
|
|
175
193
|
class ParseCustomClassNoCreateMethodError < ParseError
|
176
194
|
def initialize(line, class_name)
|
177
|
-
super(line, line.indentation,
|
195
|
+
super(line, line.indentation,
|
196
|
+
"Detected an encode custom class #{class_name} "\
|
197
|
+
"but it does not have a #nt_create method, so it can't be deserialzied.")
|
178
198
|
end
|
179
199
|
end
|
180
200
|
|
@@ -182,13 +202,13 @@ module NestedText
|
|
182
202
|
|
183
203
|
class AssertionLineScannerIsEmptyError < AssertionError
|
184
204
|
def initialize
|
185
|
-
super(
|
205
|
+
super('There is no more input to consume. You should have checked this with #empty? before calling.')
|
186
206
|
end
|
187
207
|
end
|
188
208
|
|
189
209
|
class AssertionInlineScannerIsEmptyError < AssertionError
|
190
210
|
def initialize
|
191
|
-
super(
|
211
|
+
super('There is no more input to consume. You should have checked this with #empty? before calling.')
|
192
212
|
end
|
193
213
|
end
|
194
214
|
|
@@ -206,20 +226,20 @@ module NestedText
|
|
206
226
|
class DumpUnsupportedTypeError < DumpError
|
207
227
|
def initialize(obj, culprit)
|
208
228
|
# Needed to pass official test.
|
209
|
-
class_name = obj.is_a?(Integer) ?
|
229
|
+
class_name = obj.is_a?(Integer) ? 'int' : obj.class.name
|
210
230
|
super(culprit, "unsupported type (#{class_name}).")
|
211
231
|
end
|
212
232
|
end
|
213
233
|
|
214
234
|
class DumpCyclicReferencesDetectedError < DumpError
|
215
235
|
def initialize(culprit)
|
216
|
-
super(culprit,
|
236
|
+
super(culprit, 'cyclic reference found: cannot be dumped.')
|
217
237
|
end
|
218
238
|
end
|
219
239
|
|
220
240
|
class DumpHashKeyStrictStringError < DumpError
|
221
241
|
def initialize(obj)
|
222
|
-
super(obj,
|
242
|
+
super(obj, 'keys must be strings.')
|
223
243
|
end
|
224
244
|
end
|
225
245
|
|
@@ -232,25 +252,29 @@ module NestedText
|
|
232
252
|
|
233
253
|
class UnsupportedTopLevelTypeError < InternalError
|
234
254
|
def initialize(type_class)
|
235
|
-
super("The given top level type #{type_class&.name} is unsupported.
|
255
|
+
super("The given top level type #{type_class&.name} is unsupported." \
|
256
|
+
"Chose between #{TOP_LEVEL_TYPES.join(', ')}.")
|
236
257
|
end
|
237
258
|
end
|
238
259
|
|
239
260
|
class WrongInputTypeError < InternalError
|
240
261
|
def initialize(class_exps, class_act)
|
241
|
-
super("The given input type #{class_act.class.name} is unsupported.
|
262
|
+
super("The given input type #{class_act.class.name} is unsupported. " \
|
263
|
+
"Expected to be of types #{class_exps.map(&:name).join(', ')}")
|
242
264
|
end
|
243
265
|
end
|
244
266
|
|
245
267
|
class TopLevelTypeMismatchParsedTypeError < InternalError
|
246
268
|
def initialize(class_exp, class_act)
|
247
|
-
super("The requested top level class #{class_exp.name}
|
269
|
+
super("The requested top level class #{class_exp.name} " \
|
270
|
+
"is not the same as the actual parsed top level class #{class_act}.")
|
248
271
|
end
|
249
272
|
end
|
250
273
|
|
251
274
|
class DumpBadIOError < InternalError
|
252
275
|
def initialize(io)
|
253
|
-
super(
|
276
|
+
super('When giving the io argument, it must be of type IO (respond to #write, #fsync).' \
|
277
|
+
" Given: #{io.class.name}")
|
254
278
|
end
|
255
279
|
end
|
256
280
|
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nestedtext/errors_internal'
|
4
|
+
require 'nestedtext/scanners'
|
5
|
+
|
6
|
+
module NestedText
|
7
|
+
# A LL(1) recursive descent parser for inline NT types.
|
8
|
+
class InlineParser # rubocop:disable Metrics/ClassLength
|
9
|
+
def initialize(line)
|
10
|
+
@inline_scanner = InlineScanner.new(line)
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
result = parse_any
|
15
|
+
unless @inline_scanner.empty?
|
16
|
+
raise Errors::ParseInlineExtraCharactersAfterDelimiterError.new(@inline_scanner.line, @inline_scanner.pos,
|
17
|
+
@inline_scanner.remaining)
|
18
|
+
end
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parse_any
|
25
|
+
return nil if @inline_scanner.peek.nil?
|
26
|
+
|
27
|
+
consume_whitespaces # Leading
|
28
|
+
result = case @inline_scanner.peek
|
29
|
+
when '{'
|
30
|
+
parse_dict
|
31
|
+
when '['
|
32
|
+
parse_list
|
33
|
+
else # Inline string
|
34
|
+
parse_string
|
35
|
+
end
|
36
|
+
|
37
|
+
consume_whitespaces # Trailing
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def consume_whitespaces
|
42
|
+
@inline_scanner.read_next while !@inline_scanner.empty? && [' ', "\t"].include?(@inline_scanner.peek)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_key_last_char(key)
|
46
|
+
last_char = @inline_scanner.read_next
|
47
|
+
if last_char == '}' && key.empty?
|
48
|
+
raise Errors::ParseInlineMissingValueError.new(@inline_scanner.line, @inline_scanner.pos - 1)
|
49
|
+
end
|
50
|
+
return if last_char == ':'
|
51
|
+
|
52
|
+
raise Errors::ParseInlineDictKeySyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1, last_char)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_key
|
56
|
+
key = []
|
57
|
+
until @inline_scanner.empty? || [':', '{', '}', '[', ']', ','].include?(@inline_scanner.peek)
|
58
|
+
key << @inline_scanner.read_next
|
59
|
+
end
|
60
|
+
if @inline_scanner.empty?
|
61
|
+
raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
|
62
|
+
@inline_scanner.pos)
|
63
|
+
end
|
64
|
+
parse_key_last_char(key)
|
65
|
+
key.join.strip
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_dict_last_char
|
69
|
+
last_char = @inline_scanner.read_next
|
70
|
+
return if last_char == '}'
|
71
|
+
|
72
|
+
raise Errors::ParseInlineDictSyntaxError.new(@inline_scanner.line,
|
73
|
+
@inline_scanner.pos - 1, last_char)
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_dict
|
77
|
+
result = {}
|
78
|
+
loop do
|
79
|
+
@inline_scanner.read_next
|
80
|
+
break if result.empty? && @inline_scanner.peek == '}'
|
81
|
+
|
82
|
+
key = parse_key
|
83
|
+
value = parse_any
|
84
|
+
result[key] = value
|
85
|
+
break unless @inline_scanner.peek == ','
|
86
|
+
end
|
87
|
+
if @inline_scanner.empty?
|
88
|
+
raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
|
89
|
+
@inline_scanner.pos)
|
90
|
+
end
|
91
|
+
parse_dict_last_char
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_list_last_char(result)
|
96
|
+
last_char = @inline_scanner.read_next
|
97
|
+
return unless last_char != ']'
|
98
|
+
|
99
|
+
if result[-1] == ''
|
100
|
+
raise Errors::ParseInlineMissingValueError.new(@inline_scanner.line,
|
101
|
+
@inline_scanner.pos - 1)
|
102
|
+
else
|
103
|
+
raise Errors::ParseInlineListSyntaxError.new(@inline_scanner.line,
|
104
|
+
@inline_scanner.pos - 1, last_char)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def parse_list
|
109
|
+
result = []
|
110
|
+
loop do
|
111
|
+
@inline_scanner.read_next
|
112
|
+
break if result.empty? && @inline_scanner.peek == ']'
|
113
|
+
|
114
|
+
result << parse_any
|
115
|
+
break unless @inline_scanner.peek == ','
|
116
|
+
end
|
117
|
+
if @inline_scanner.empty?
|
118
|
+
raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
|
119
|
+
@inline_scanner.pos)
|
120
|
+
end
|
121
|
+
parse_list_last_char(result)
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_string
|
126
|
+
inline_string = []
|
127
|
+
until @inline_scanner.empty? || ['{', '}', '[', ']', ','].include?(@inline_scanner.peek)
|
128
|
+
inline_string << @inline_scanner.read_next
|
129
|
+
end
|
130
|
+
inline_string.join.rstrip # Trim trailing whitespaces that lead up to next break point.
|
131
|
+
end
|
132
|
+
end
|
133
|
+
private_constant :InlineParser
|
134
|
+
end
|