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.
@@ -1,15 +1,15 @@
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
5
 
6
- require "nestedtext/constants"
7
- require "nestedtext/error"
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 instansiating.
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 pretty_message(line)
30
- lineno_disp = @lineno + 1
31
- colno_disp = @colno + 1
32
- prefix = "\nParse ParseError (line #{lineno_disp}, column #{colno_disp}): "
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
- 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}"
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
- marker_indent = colno_disp + digits # +1 for the "|"
46
- marker = "\n\t" + " " * marker_indent + "^"
60
+ def pretty_marker
61
+ marker_indent = colno_disp + lineno_digits # +1 for the "|"
62
+ "\n\t#{' ' * marker_indent}^"
63
+ end
47
64
 
48
- prefix + @message_raw + last_lines + marker
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, "unrecognized line.")
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, "expected list item.")
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, "multiline key requires a value.")
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, "expected value.")
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, "line ended without closing delimiter.")
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 ? "characters" : "character"
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 == 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
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, "The current line was detected to be #{type_act}, but we expected to see any of [#{type_exps.join(", ")}] here.")
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, "expected dictionary item.")
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 compatialbe with official tests.
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 = " (NO-BREAK SPACE)"
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["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