nestedtext 3.2.1 → 4.2.0

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