nestedtext 3.2.1 → 4.0.0

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