nestedtext 3.2.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|