nestedtext 0.1.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.
- 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
|