nestedtext 3.2.1 → 4.2.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.
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "word_wrap"
4
- require "word_wrap/core_ext"
3
+ require 'word_wrap'
4
+ require 'word_wrap/core_ext'
5
+ require 'unicode_utils'
5
6
 
6
- require "nestedtext/constants"
7
- require "nestedtext/error"
7
+ require 'nestedtext/constants'
8
+ require 'nestedtext/error'
8
9
 
9
10
  module NestedText
10
11
  module Errors
11
12
  class InternalError < Error
12
- public_class_method :new # Prevent users from instansiating.
13
+ public_class_method :new # Prevent users from instantiating.
13
14
  end
14
15
 
15
16
  class ParseError < InternalError
@@ -26,26 +27,44 @@ module NestedText
26
27
 
27
28
  private
28
29
 
29
- def pretty_message(line)
30
- lineno_disp = @lineno + 1
31
- colno_disp = @colno + 1
32
- prefix = "\nParse ParseError (line #{lineno_disp}, column #{colno_disp}): "
30
+ def lineno_disp
31
+ @lineno + 1
32
+ end
33
+
34
+ def colno_disp
35
+ @colno + 1
36
+ end
37
+
38
+ # Number of digits in the line number.
39
+ def lineno_digits
40
+ lineno_disp.to_s.length
41
+ end
42
+
43
+ def pretty_prefix
44
+ "\nParse ParseError (line #{lineno_disp}, column #{colno_disp}): "
45
+ end
46
+
47
+ def pretty_line(line)
48
+ return '' if line.nil?
49
+
50
+ lline_indent = ' ' * line.indentation
51
+ prev_lineno_disp = line.lineno + 1
33
52
 
34
- last_lines = ""
35
53
  # From one line to another, we can at most have 1 digits length difference.
36
- digits = lineno_disp.to_s.length
37
- unless line.prev.nil?
38
- lline_indent = " " * line.prev.indentation
39
- prev_lineno_disp = line.prev.lineno + 1
40
- last_lines += "\n\t#{prev_lineno_disp.to_s.rjust(digits)}│#{lline_indent}#{line.prev.content}"
41
- end
42
- line_indent = " " * line.indentation
43
- last_lines += "\n\t#{lineno_disp}│#{line_indent}#{line.content}"
54
+ "\n\t#{prev_lineno_disp.to_s.rjust(lineno_digits)}│#{lline_indent}#{line.content}"
55
+ end
44
56
 
45
- marker_indent = colno_disp + digits # +1 for the "|"
46
- marker = "\n\t" + " " * marker_indent + "^"
57
+ def pretty_last_lines(line)
58
+ pretty_line(line.prev) + pretty_line(line)
59
+ end
47
60
 
48
- prefix + @message_raw + last_lines + marker
61
+ def pretty_marker
62
+ marker_indent = colno_disp + lineno_digits # +1 for the "|"
63
+ "\n\t#{' ' * marker_indent}^"
64
+ end
65
+
66
+ def pretty_message(line)
67
+ pretty_prefix + @message_raw + pretty_last_lines(line) + pretty_marker
49
68
  end
50
69
  end
51
70
 
@@ -57,19 +76,19 @@ module NestedText
57
76
 
58
77
  class ParseLineTagNotDetectedError < ParseError
59
78
  def initialize(line)
60
- super(line, line.indentation, "unrecognized line.")
79
+ super(line, line.indentation, 'unrecognized line.')
61
80
  end
62
81
  end
63
82
 
64
83
  class ParseLineTypeExpectedListItemError < ParseError
65
84
  def initialize(line)
66
- super(line, line.indentation, "expected list item.")
85
+ super(line, line.indentation, 'expected list item.')
67
86
  end
68
87
  end
69
88
 
70
89
  class ParseMultilineKeyNoValueError < ParseError
71
90
  def initialize(line)
72
- super(line, line.indentation, "multiline key requires a value.")
91
+ super(line, line.indentation, 'multiline key requires a value.')
73
92
  end
74
93
  end
75
94
 
@@ -87,7 +106,7 @@ module NestedText
87
106
 
88
107
  class ParseInlineMissingValueError < ParseError
89
108
  def initialize(line, colno)
90
- super(line, line.indentation + colno, "expected value.")
109
+ super(line, line.indentation + colno, 'expected value.')
91
110
  end
92
111
  end
93
112
 
@@ -99,13 +118,13 @@ module NestedText
99
118
 
100
119
  class ParseInlineNoClosingDelimiterError < ParseError
101
120
  def initialize(line, colno)
102
- super(line, line.indentation + colno, "line ended without closing delimiter.")
121
+ super(line, line.indentation + colno, 'line ended without closing delimiter.')
103
122
  end
104
123
  end
105
124
 
106
125
  class ParseInlineExtraCharactersAfterDelimiterError < ParseError
107
126
  def initialize(line, colno, extra_chars)
108
- character_str = extra_chars.length > 1 ? "characters" : "character"
127
+ character_str = extra_chars.length > 1 ? 'characters' : 'character'
109
128
  super(line, line.indentation + colno, "extra #{character_str} after closing delimiter: ‘#{extra_chars}’.")
110
129
  end
111
130
  end
@@ -113,19 +132,15 @@ module NestedText
113
132
  class ParseInvalidIndentationError < ParseError
114
133
  def initialize(line, ind_exp)
115
134
  prev_line = line.prev
116
- if prev_line.nil? && ind_exp == 0
117
- message = "top-level content must start in column 1."
118
- elsif !prev_line.nil? &&
119
- prev_line.attribs.key?("value") &&
120
- prev_line.indentation < line.indentation &&
121
- %i[dict_item list_item].member?(prev_line.tag)
122
- message = "invalid indentation."
123
- elsif !prev_line.nil? && line.indentation < prev_line.indentation
124
- # Can't use ind_exp here, because it's a difference if the previous line was further indented. See test_load_error_dict_10
125
- message = "invalid indentation, partial dedent."
126
- else
127
- message = "invalid indentation."
128
- end
135
+ message = if prev_line.nil? && ind_exp.zero?
136
+ 'top-level content must start in column 1.'
137
+ elsif !prev_line.nil? && line.indentation < prev_line.indentation
138
+ # Can't use ind_exp here, because it's a difference if the previous line was further indented.
139
+ # See test_load_error_dict_10
140
+ 'invalid indentation, partial dedent.'
141
+ else
142
+ 'invalid indentation.'
143
+ end
129
144
  # Need to wrap like official tests. #wrap always add an extra \n we need to chop off.
130
145
  message_wrapped = message.wrap(70).chop
131
146
  super(line, ind_exp, message_wrapped)
@@ -134,26 +149,27 @@ module NestedText
134
149
 
135
150
  class ParseLineTypeNotExpectedError < ParseError
136
151
  def initialize(line, type_exps, type_act)
137
- super(line, line.indentation, "The current line was detected to be #{type_act}, but we expected to see any of [#{type_exps.join(", ")}] here.")
152
+ super(line, line.indentation,
153
+ "The current line was detected to be #{type_act}, "\
154
+ "but we expected to see any of [#{type_exps.join(', ')}] here.")
138
155
  end
139
156
  end
140
157
 
141
158
  class ParseLineTypeExpectedDictItemError < ParseError
142
159
  def initialize(line)
143
- super(line, line.indentation, "expected dictionary item.")
160
+ super(line, line.indentation, 'expected dictionary item.')
144
161
  end
145
162
  end
146
163
 
147
164
  class ParseInvalidIndentationCharError < ParseError
148
165
  def initialize(line)
149
- printable_char = line.content[0].dump.gsub(/"/, "")
166
+ char = line.content[0]
167
+ # Official-test kludge; Translate rubys \u00 to python's unicodedata.name \x format.
168
+ printable_char = char.dump.gsub(/"/, '').gsub(/\\u0*/, '\x').downcase
150
169
 
151
- # Looking for non-breaking space is just to be compatialbe with official tests.
152
- explanation = ""
153
- if printable_char == '\\u00A0'
154
- printable_char = '\\xa0'
155
- explanation = " (NO-BREAK SPACE)"
156
- end
170
+ explanation = ''
171
+ # Official-test kludge; ASCII chars have printable names too, but they are not used in reference implementation.
172
+ explanation = " (#{UnicodeUtils.char_name(char)})" unless char.ord < 128
157
173
 
158
174
  message = "invalid character in indentation: '#{printable_char}'#{explanation}."
159
175
  super(line, line.indentation, message)
@@ -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["key"]}.")
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, "Detected an encode custom class #{class_name} however we can't find it, so it can't be deserialzied.")
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, "Detected an encode custom class #{class_name} but it does not have a #nt_create method, so it can't be deserialzied.")
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("There is no more input to consume. You should have checked this with #empty? before calling.")
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("There is no more input to consume. You should have checked this with #empty? before calling.")
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) ? "int" : obj.class.name
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, "cyclic reference found: cannot be dumped.")
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, "keys must be strings.")
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. Chose between #{TOP_LEVEL_TYPES.join(", ")}.")
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. Expected to be of types #{class_exps.map(&:name).join(", ")}")
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} is not the same as the actual parsed top level class #{class_act}.")
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("When giving the io argument, it must be of type IO (respond to #write, #fsync). Given: #{io.class.name}")
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