nestedtext 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +24 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +143 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +4 -0
- data/Gemfile +31 -0
- data/LICENSE.txt +21 -0
- data/OSSMETADATA +1 -0
- data/README.md +113 -0
- data/SECURITY.md +12 -0
- data/lib/nestedtext/constants.rb +5 -0
- data/lib/nestedtext/core_ext.rb +18 -0
- data/lib/nestedtext/decode.rb +33 -0
- data/lib/nestedtext/dumper.rb +177 -0
- data/lib/nestedtext/encode.rb +35 -0
- data/lib/nestedtext/encode_helpers.rb +24 -0
- data/lib/nestedtext/errors.rb +262 -0
- data/lib/nestedtext/helpers.rb +7 -0
- data/lib/nestedtext/parser.rb +287 -0
- data/lib/nestedtext/scanners.rb +161 -0
- data/lib/nestedtext/version.rb +5 -0
- data/lib/nestedtext.rb +8 -0
- data/nestedtext.gemspec +27 -0
- metadata +77 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require "nestedtext/dumper"
|
2
|
+
|
3
|
+
module NestedText
|
4
|
+
module NTEncodeStrictMixing
|
5
|
+
def to_nt(indentation: 4, strict: true)
|
6
|
+
Dumper.new(EncodeOptions.new(indentation, strict)).dump self
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module NTEncodeMixing
|
11
|
+
def to_nt(indentation: 4)
|
12
|
+
Dumper.new(EncodeOptions.new(indentation, false)).dump self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class EncodeOptions
|
17
|
+
attr_reader :indentation, :strict
|
18
|
+
|
19
|
+
def initialize(indentation = 4, strict = true)
|
20
|
+
@indentation = indentation
|
21
|
+
@strict = strict
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "word_wrap"
|
4
|
+
require "word_wrap/core_ext"
|
5
|
+
|
6
|
+
require "nestedtext/constants"
|
7
|
+
|
8
|
+
module NestedText
|
9
|
+
# Top level ParseError for clients to rescue.
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
module Errors
|
13
|
+
# TODO: rename all Subclasses to ParseXError, just like for Dump
|
14
|
+
class ParseError < Error
|
15
|
+
attr_reader :lineno, :colno, :message_raw
|
16
|
+
|
17
|
+
def initialize(line, colno, message)
|
18
|
+
# Note, both line and column number are 0-indexed.
|
19
|
+
# But for human display we make them 1-indexed.
|
20
|
+
@lineno = line.lineno
|
21
|
+
@colno = colno
|
22
|
+
@message_raw = message
|
23
|
+
super(pretty_message(line))
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def pretty_message(line)
|
29
|
+
lineno_disp = @lineno + 1
|
30
|
+
colno_disp = @colno + 1
|
31
|
+
prefix = "\nParse ParseError (line #{lineno_disp}, column #{colno_disp}): "
|
32
|
+
|
33
|
+
last_lines = ""
|
34
|
+
# From one line to another, we can at most have 1 digits length difference.
|
35
|
+
digits = lineno_disp.to_s.length
|
36
|
+
unless line.prev.nil?
|
37
|
+
lline_indent = " " * line.prev.indentation
|
38
|
+
prev_lineno_disp = line.prev.lineno + 1
|
39
|
+
last_lines += "\n\t#{prev_lineno_disp.to_s.rjust(digits)}│#{lline_indent}#{line.prev.content}"
|
40
|
+
end
|
41
|
+
line_indent = " " * line.indentation
|
42
|
+
last_lines += "\n\t#{lineno_disp}│#{line_indent}#{line.content}"
|
43
|
+
|
44
|
+
marker_indent = colno_disp + digits # +1 for the "|"
|
45
|
+
marker = "\n\t" + " " * marker_indent + "^"
|
46
|
+
|
47
|
+
prefix + @message_raw + last_lines + marker
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class LineTagUnknown < ParseError
|
52
|
+
def initialize(line, tag)
|
53
|
+
super(line, line.indentation, "The Line tag #{tag} is not among the allowed ones #{Line::ALLOWED_LINE_TAGS}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class LineTagNotDetected < ParseError
|
58
|
+
def initialize(line)
|
59
|
+
super(line, line.indentation, "unrecognized line.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class LineTypeExpectedListItem < ParseError
|
64
|
+
def initialize(line)
|
65
|
+
super(line, line.indentation, "expected list item.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class MultilineKeyNoValue < ParseError
|
70
|
+
def initialize(line)
|
71
|
+
super(line, line.indentation, "multiline key requires a value.")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class InlineDictSyntaxError < ParseError
|
76
|
+
def initialize(line, colno, wrong_char)
|
77
|
+
super(line, line.indentation + colno, "expected ‘,’ or ‘}’, found ‘#{wrong_char}’.")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class InlineDictKeySyntaxError < ParseError
|
82
|
+
def initialize(line, colno, wrong_char)
|
83
|
+
super(line, line.indentation + colno, "expected ‘:’, found ‘#{wrong_char}’.")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class InlineMissingValue < ParseError
|
88
|
+
def initialize(line, colno)
|
89
|
+
super(line, line.indentation + colno, "expected value.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class InlineListSyntaxError < ParseError
|
94
|
+
def initialize(line, colno, wrong_char)
|
95
|
+
super(line, line.indentation + colno, "expected ‘,’ or ‘]’, found ‘#{wrong_char}’.")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class InlineNoClosingDelimiter < ParseError
|
100
|
+
def initialize(line, colno)
|
101
|
+
super(line, line.indentation + colno, "line ended without closing delimiter.")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class InlineExtraCharactersAfterDelimiter < ParseError
|
106
|
+
def initialize(line, colno, extra_chars)
|
107
|
+
character_str = extra_chars.length > 1 ? "characters" : "character"
|
108
|
+
super(line, line.indentation + colno, "extra #{character_str} after closing delimiter: ‘#{extra_chars}’.")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class InvalidIndentation < ParseError
|
113
|
+
def initialize(line, ind_exp)
|
114
|
+
prev_line = line.prev
|
115
|
+
if prev_line.nil? && ind_exp == 0
|
116
|
+
message = "top-level content must start in column 1."
|
117
|
+
elsif !prev_line.nil? &&
|
118
|
+
prev_line.attribs.key?("value") &&
|
119
|
+
prev_line.indentation < line.indentation &&
|
120
|
+
%i[dict_item list_item].member?(prev_line.tag)
|
121
|
+
message = "invalid indentation."
|
122
|
+
elsif !prev_line.nil? && line.indentation < prev_line.indentation
|
123
|
+
# Can't use ind_exp here, because it's a difference if the previous line was further indented. See test_load_error_dict_10
|
124
|
+
message = "invalid indentation, partial dedent."
|
125
|
+
else
|
126
|
+
message = "invalid indentation."
|
127
|
+
end
|
128
|
+
# Need to wrap like official tests. #wrap always add an extra \n we need to chop off.
|
129
|
+
message_wrapped = message.wrap(70).chop
|
130
|
+
super(line, ind_exp, message_wrapped)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class LineTypeNotExpected < ParseError
|
135
|
+
def initialize(line, type_exps, type_act)
|
136
|
+
super(line, line.indentation, "The current line was detected to be #{type_act}, but we expected to see any of [#{type_exps.join(", ")}] here.")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class LineTypeExpectedDictItem < ParseError
|
141
|
+
def initialize(line)
|
142
|
+
super(line, line.indentation, "expected dictionary item.")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class InvalidIndentationChar < ParseError
|
147
|
+
def initialize(line)
|
148
|
+
printable_char = line.content[0].dump.gsub(/"/, "")
|
149
|
+
|
150
|
+
# Looking for non-breaking space is just to be compatialbe with official tests.
|
151
|
+
explanation = ""
|
152
|
+
if printable_char == '\\u00A0'
|
153
|
+
printable_char = '\\xa0'
|
154
|
+
explanation = " (NO-BREAK SPACE)"
|
155
|
+
end
|
156
|
+
|
157
|
+
message = "invalid character in indentation: '#{printable_char}'#{explanation}."
|
158
|
+
super(line, line.indentation, message)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class DictDuplicateKey < ParseError
|
163
|
+
def initialize(line)
|
164
|
+
super(line, line.indentation, "duplicate key: #{line.attribs["key"]}.")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class ParseCustomClassNotFound < ParseError
|
169
|
+
def initialize(line, class_name)
|
170
|
+
super(line, line.indentation, "Detected an encode custom class #{class_name} however we can't find it, so it can't be deserialzied.")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ParseCustomClassNoCreateMethod < ParseError
|
175
|
+
def initialize(line, class_name)
|
176
|
+
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.")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class UnsupportedTopLevelTypeError < Error
|
181
|
+
def initialize(type_class)
|
182
|
+
super("The given top level type #{type_class&.name} is unsupported. Chose between #{TOP_LEVEL_TYPES.join(", ")}.")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
class WrongInputTypeError < Error
|
187
|
+
def initialize(class_exps, class_act)
|
188
|
+
super("The given input type #{class_act.class.name} is unsupported. Expected to be of types #{class_exps.map(&:name).join(", ")}")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class TopLevelTypeMismatchParsedType < Error
|
193
|
+
def initialize(class_exp, class_act)
|
194
|
+
super("The requested top level class #{class_exp.name} is not the same as the actual parsed top level class #{class_act}.")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class AssertionError < Error; end
|
199
|
+
|
200
|
+
class LineScannerIsEmpty < AssertionError
|
201
|
+
def initialize
|
202
|
+
super("There is no more input to consume. You should have checked this with #empty? before calling.")
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class InlineScannerIsEmpty < AssertionError
|
207
|
+
def initialize
|
208
|
+
super("There is no more input to consume. You should have checked this with #empty? before calling.")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class DumpBadIO < Error
|
213
|
+
def initialize(io)
|
214
|
+
super("When giving the io argument, it must be of type IO (respond to #write, #fsync). Given: #{io.class.name}")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class DumpFileBadPath < Error
|
219
|
+
def initialize(path)
|
220
|
+
super("Must supply a string to a file path that can be written to. Given: #{path}")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class DumpError < Error
|
225
|
+
attr_reader :culprit
|
226
|
+
|
227
|
+
def initialize(culprit, message)
|
228
|
+
# Note, both line and column number are 0-indexed.
|
229
|
+
# But for human display we make them 1-indexed.
|
230
|
+
@culprit = culprit
|
231
|
+
super(message)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class DumpUnsupportedTypeError < DumpError
|
236
|
+
def initialize(obj, culprit)
|
237
|
+
# Needed to pass official test.
|
238
|
+
class_name = obj.is_a?(Integer) ? "int" : obj.class.name
|
239
|
+
super(culprit, "unsupported type (#{class_name}).")
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class DumpCyclicReferencesDetected < DumpError
|
244
|
+
def initialize(culprit)
|
245
|
+
super(culprit, "cyclic reference found: cannot be dumped.")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class DumpHashKeyStrictString < DumpError
|
250
|
+
def initialize(obj)
|
251
|
+
super(obj, "keys must be strings.")
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.raise_unrecognized_line(line)
|
256
|
+
# [[:space:]] include all Unicode spaces e.g. non-breakable space which \s does not.
|
257
|
+
raise InvalidIndentationChar, line if line.content.chr =~ /[[:space:]]/
|
258
|
+
|
259
|
+
raise LineTagNotDetected, line
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
require "nestedtext/errors"
|
6
|
+
require "nestedtext/scanners"
|
7
|
+
require "nestedtext/helpers"
|
8
|
+
|
9
|
+
module NestedText
|
10
|
+
class Parser
|
11
|
+
# TODO: document that caller is responsible for closing IO after done with Parser.
|
12
|
+
def initialize(io, top_class, strict: true)
|
13
|
+
assert_valid_input_type io
|
14
|
+
NestedText.assert_valid_top_level_type(top_class)
|
15
|
+
@top_class = top_class
|
16
|
+
@strict = strict
|
17
|
+
@line_scanner = LineScanner.new(io)
|
18
|
+
@inline_scanner = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse
|
22
|
+
result = parse_any(0)
|
23
|
+
case @top_class.object_id
|
24
|
+
when Object.object_id
|
25
|
+
raise Errors::AssertionError, "Parsed result is of unexpected type." if
|
26
|
+
!result.nil? && ![Hash, Array, String].include?(result.class) && @strict
|
27
|
+
when Hash.object_id
|
28
|
+
result = {} if result.nil?
|
29
|
+
raise Errors::TopLevelTypeMismatchParsedType.new(@top_class, result) unless result.instance_of?(Hash)
|
30
|
+
when Array.object_id
|
31
|
+
result = [] if result.nil?
|
32
|
+
raise Errors::TopLevelTypeMismatchParsedType.new(@top_class, result) unless result.instance_of?(Array)
|
33
|
+
when String.object_id
|
34
|
+
result = "" if result.nil?
|
35
|
+
raise Errors::TopLevelTypeMismatchParsedType.new(@top_class, result) unless result.instance_of?(String)
|
36
|
+
else
|
37
|
+
raise Errors::UnsupportedTopLevelTypeError, @top_class
|
38
|
+
end
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def assert_valid_input_type(input)
|
45
|
+
unless input.nil? || input.is_a?(IO) || input.is_a?(StringIO)
|
46
|
+
raise Errors::WrongInputTypeError.new([IO, StringIO], input)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_any(indentation)
|
51
|
+
return nil if @line_scanner.peek.nil?
|
52
|
+
|
53
|
+
case @line_scanner.peek.tag # TODO: Use Null Pattern instead with a EndOfInput tag?
|
54
|
+
when :list_item
|
55
|
+
parse_list_item(indentation)
|
56
|
+
when :dict_item, :key_item
|
57
|
+
parse_dict_item(indentation)
|
58
|
+
when :string_item
|
59
|
+
parse_string_item(indentation)
|
60
|
+
when :inline_dict
|
61
|
+
parse_inline_dict
|
62
|
+
when :inline_list
|
63
|
+
parse_inline_list
|
64
|
+
when :unrecognized
|
65
|
+
Errors.raise_unrecognized_line(@line_scanner.peek)
|
66
|
+
else
|
67
|
+
raise Errors::AssertionError, "Unexpected line tag! #{@line_scanner.peek.tag}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_list_item(indentation)
|
72
|
+
result = []
|
73
|
+
while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
|
74
|
+
line = @line_scanner.read_next
|
75
|
+
|
76
|
+
Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
|
77
|
+
raise Errors::LineTypeExpectedListItem, line unless line.tag == :list_item
|
78
|
+
raise Errors::InvalidIndentation.new(line, indentation) if line.indentation != indentation
|
79
|
+
|
80
|
+
value = line.attribs["value"]
|
81
|
+
if value.nil?
|
82
|
+
if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
|
83
|
+
value = parse_any(@line_scanner.peek.indentation)
|
84
|
+
elsif @line_scanner.peek.nil? || @line_scanner.peek.tag == :list_item
|
85
|
+
value = ""
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
result << value
|
90
|
+
end
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_dict_item(indentation)
|
95
|
+
result = {}
|
96
|
+
first_line = nil
|
97
|
+
while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
|
98
|
+
line = @line_scanner.read_next
|
99
|
+
first_line = line if first_line.nil?
|
100
|
+
Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
|
101
|
+
raise Errors::InvalidIndentation.new(line, indentation) if line.indentation != indentation
|
102
|
+
raise Errors::LineTypeExpectedDictItem, line unless %i[dict_item key_item].include? line.tag
|
103
|
+
|
104
|
+
value = nil
|
105
|
+
key = nil
|
106
|
+
if line.tag == :dict_item
|
107
|
+
key = line.attribs["key"]
|
108
|
+
value = line.attribs["value"]
|
109
|
+
if value.nil?
|
110
|
+
value = ""
|
111
|
+
if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
|
112
|
+
value = parse_any(@line_scanner.peek.indentation)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
else # :key_item
|
116
|
+
key = line.attribs["key"]
|
117
|
+
while @line_scanner.peek&.tag == :key_item && @line_scanner.peek.indentation == indentation
|
118
|
+
line = @line_scanner.read_next
|
119
|
+
key += "\n" + line.attribs["key"]
|
120
|
+
end
|
121
|
+
exp_types = %i[dict_item key_item list_item string_item]
|
122
|
+
if @line_scanner.peek.nil?
|
123
|
+
value = ""
|
124
|
+
else
|
125
|
+
unless exp_types.member?(@line_scanner.peek.tag)
|
126
|
+
raise Errors::LineTypeNotExpected.new(line, exp_types, line.tag)
|
127
|
+
end
|
128
|
+
raise Errors::MultilineKeyNoValue, line unless @line_scanner.peek.indentation > indentation
|
129
|
+
|
130
|
+
value = parse_any(@line_scanner.peek.indentation)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
raise Errors::DictDuplicateKey, line if result.key? key
|
134
|
+
|
135
|
+
result[key] = value
|
136
|
+
end
|
137
|
+
|
138
|
+
# Custom class decoding.
|
139
|
+
if !@strict && result.length == 2 && result.key?(CUSTOM_CLASS_KEY)
|
140
|
+
class_name = result[CUSTOM_CLASS_KEY]
|
141
|
+
begin
|
142
|
+
clazz = class_name == "nil" ? NilClass : Object.const_get(class_name, false)
|
143
|
+
rescue NameError
|
144
|
+
raise Errors::ParseCustomClassNotFound.new(first_line, class_name)
|
145
|
+
end
|
146
|
+
if clazz.respond_to? :nt_create
|
147
|
+
result = clazz.nt_create(result["data"])
|
148
|
+
else
|
149
|
+
raise Errors::ParseCustomClassNoCreateMethod.new(first_line, class_name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
def parse_string_item(indentation)
|
157
|
+
result = []
|
158
|
+
while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
|
159
|
+
line = @line_scanner.read_next
|
160
|
+
raise Errors::InvalidIndentation.new(line, indentation) if line.indentation != indentation
|
161
|
+
raise Errors::LineTypeNotExpected.new(line, %i[string_item], line.tag) unless line.tag == :string_item
|
162
|
+
|
163
|
+
value = line.attribs["value"]
|
164
|
+
result << value
|
165
|
+
end
|
166
|
+
result.join("\n")
|
167
|
+
end
|
168
|
+
|
169
|
+
def parse_inline_key
|
170
|
+
key = []
|
171
|
+
until @inline_scanner.empty? || [":", "{", "}", "[", "]", ","].include?(@inline_scanner.peek)
|
172
|
+
key << @inline_scanner.read_next
|
173
|
+
end
|
174
|
+
if @inline_scanner.empty?
|
175
|
+
raise Errors::InlineNoClosingDelimiter.new(@inline_scanner.line,
|
176
|
+
@inline_scanner.pos)
|
177
|
+
end
|
178
|
+
|
179
|
+
last_char = @inline_scanner.read_next
|
180
|
+
if last_char == "}" && key.empty?
|
181
|
+
raise Errors::InlineMissingValue.new(@inline_scanner.line, @inline_scanner.pos - 1)
|
182
|
+
end
|
183
|
+
unless last_char == ":"
|
184
|
+
raise Errors::InlineDictKeySyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1, last_char)
|
185
|
+
end
|
186
|
+
|
187
|
+
key.join.strip
|
188
|
+
end
|
189
|
+
|
190
|
+
def parse_inline
|
191
|
+
return nil if @inline_scanner.peek.nil?
|
192
|
+
|
193
|
+
result = nil
|
194
|
+
# Trim leading whitespaces
|
195
|
+
@inline_scanner.read_next while !@inline_scanner.empty? && [" ", "\t"].include?(@inline_scanner.peek)
|
196
|
+
case @inline_scanner.peek
|
197
|
+
when "{"
|
198
|
+
result = {}
|
199
|
+
first = true
|
200
|
+
loop do
|
201
|
+
@inline_scanner.read_next
|
202
|
+
break if first && @inline_scanner.peek == "}"
|
203
|
+
|
204
|
+
first = false
|
205
|
+
key = parse_inline_key
|
206
|
+
value = parse_inline
|
207
|
+
result[key] = value
|
208
|
+
break unless @inline_scanner.peek == ","
|
209
|
+
end
|
210
|
+
if @inline_scanner.empty?
|
211
|
+
raise Errors::InlineNoClosingDelimiter.new(@inline_scanner.line,
|
212
|
+
@inline_scanner.pos)
|
213
|
+
end
|
214
|
+
last_char = @inline_scanner.read_next
|
215
|
+
unless last_char == "}"
|
216
|
+
raise Errors::InlineDictSyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1,
|
217
|
+
last_char)
|
218
|
+
end
|
219
|
+
|
220
|
+
when "["
|
221
|
+
result = []
|
222
|
+
first = true # TODO: can be replaced by checking result.empty? below?
|
223
|
+
loop do
|
224
|
+
@inline_scanner.read_next
|
225
|
+
break if first && @inline_scanner.peek == "]"
|
226
|
+
|
227
|
+
first = false
|
228
|
+
result << parse_inline
|
229
|
+
break unless @inline_scanner.peek == ","
|
230
|
+
end
|
231
|
+
if @inline_scanner.empty?
|
232
|
+
raise Errors::InlineNoClosingDelimiter.new(@inline_scanner.line,
|
233
|
+
@inline_scanner.pos)
|
234
|
+
end
|
235
|
+
last_char = @inline_scanner.read_next
|
236
|
+
|
237
|
+
if last_char != "]"
|
238
|
+
if result[-1] == ""
|
239
|
+
raise Errors::InlineMissingValue.new(@inline_scanner.line, @inline_scanner.pos - 1)
|
240
|
+
else
|
241
|
+
raise Errors::InlineListSyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1,
|
242
|
+
last_char)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
else # Inline string
|
246
|
+
inline_string = []
|
247
|
+
until @inline_scanner.empty? || ["{", "}", "[", "]", ","].include?(@inline_scanner.peek)
|
248
|
+
inline_string << @inline_scanner.read_next
|
249
|
+
end
|
250
|
+
result = inline_string.join.rstrip # Trim trailing whitespaces that lead up to next break point.
|
251
|
+
end
|
252
|
+
# Trim trailing whitespaces
|
253
|
+
@inline_scanner.read_next while !@inline_scanner.empty? && [" ", "\t"].include?(@inline_scanner.peek)
|
254
|
+
result
|
255
|
+
end
|
256
|
+
|
257
|
+
def parse_inline_dict
|
258
|
+
@inline_scanner = InlineScanner.new(@line_scanner.read_next)
|
259
|
+
result = parse_inline
|
260
|
+
unless @inline_scanner.empty?
|
261
|
+
raise Errors::InlineExtraCharactersAfterDelimiter.new(@inline_scanner.line, @inline_scanner.pos,
|
262
|
+
@inline_scanner.remaining)
|
263
|
+
end
|
264
|
+
unless result.is_a? Hash
|
265
|
+
raise Errors::AssertionError,
|
266
|
+
"Expected inline value to be Hash but is #{result.class.name}"
|
267
|
+
end
|
268
|
+
|
269
|
+
result
|
270
|
+
end
|
271
|
+
|
272
|
+
def parse_inline_list
|
273
|
+
@inline_scanner = InlineScanner.new(@line_scanner.read_next)
|
274
|
+
result = parse_inline
|
275
|
+
unless @inline_scanner.empty?
|
276
|
+
raise Errors::InlineExtraCharactersAfterDelimiter.new(@inline_scanner.line, @inline_scanner.pos,
|
277
|
+
@inline_scanner.remaining)
|
278
|
+
end
|
279
|
+
unless result.is_a? Array
|
280
|
+
raise Errors::AssertionError,
|
281
|
+
"Expected inline value to be Array but is #{result.class.name}"
|
282
|
+
end
|
283
|
+
|
284
|
+
result
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|