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,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
3
+ require 'stringio'
4
4
 
5
- require "nestedtext/errors_internal"
6
- require "nestedtext/scanners"
7
- require "nestedtext/constants"
5
+ require 'nestedtext/errors_internal'
6
+ require 'nestedtext/scanners'
7
+ require 'nestedtext/constants'
8
+ require 'nestedtext/inline_parser'
8
9
 
9
10
  module NestedText
10
11
  # A LL(1) recursive descent parser for NT.
11
- class Parser
12
+ class Parser # rubocop:disable Metrics/ClassLength
12
13
  def self.assert_valid_top_level_type(top_class)
13
- unless !top_class.nil? && top_class.is_a?(Class) && TOP_LEVEL_TYPES.map(&:object_id).include?(top_class.object_id)
14
- raise Errors::UnsupportedTopLevelTypeError, top_class
14
+ if !top_class.nil? && top_class.is_a?(Class) && TOP_LEVEL_TYPES.map(&:object_id).include?(top_class.object_id)
15
+ return
15
16
  end
17
+
18
+ raise Errors::UnsupportedTopLevelTypeError, top_class
16
19
  end
17
20
 
18
21
  def initialize(io, top_class, strict: false)
@@ -21,42 +24,64 @@ module NestedText
21
24
  @top_class = top_class
22
25
  @strict = strict
23
26
  @line_scanner = LineScanner.new(io)
24
- @inline_scanner = nil
25
27
  end
26
28
 
27
29
  def parse
28
30
  result = parse_any(0)
29
31
  case @top_class.object_id
30
32
  when Object.object_id
31
- raise Errors::AssertionError, "Parsed result is of unexpected type." if
32
- !result.nil? && ![Hash, Array, String].include?(result.class) && @strict
33
+ return_object(result)
33
34
  when Hash.object_id
34
- result = {} if result.nil?
35
- raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(Hash)
35
+ return_hash(result)
36
36
  when Array.object_id
37
- result = [] if result.nil?
38
- raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(Array)
37
+ return_array(result)
39
38
  when String.object_id
40
- result = "" if result.nil?
41
- raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(String)
39
+ return_string(result)
42
40
  else
43
41
  raise Errors::UnsupportedTopLevelTypeError, @top_class
44
42
  end
45
- result
46
43
  end
47
44
 
48
45
  private
49
46
 
47
+ def return_object(result)
48
+ raise Errors::AssertionError, 'Parsed result is of unexpected type.' if \
49
+ !result.nil? && ![Hash, Array, String].include?(result.class) && @strict
50
+
51
+ result
52
+ end
53
+
54
+ def return_hash(result)
55
+ result = {} if result.nil?
56
+ raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(Hash)
57
+
58
+ result
59
+ end
60
+
61
+ def return_array(result)
62
+ result = [] if result.nil?
63
+ raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(Array)
64
+
65
+ result
66
+ end
67
+
68
+ def return_string(result)
69
+ result = '' if result.nil?
70
+ raise Errors::TopLevelTypeMismatchParsedTypeError.new(@top_class, result) unless result.instance_of?(String)
71
+
72
+ result
73
+ end
74
+
50
75
  def assert_valid_input_type(input)
51
- unless input.nil? || input.is_a?(IO) || input.is_a?(StringIO)
52
- raise Errors::WrongInputTypeError.new([IO, StringIO], input)
53
- end
76
+ return if input.nil? || input.is_a?(IO) || input.is_a?(StringIO)
77
+
78
+ raise Errors::WrongInputTypeError.new([IO, StringIO], input)
54
79
  end
55
80
 
56
81
  def parse_any(indentation)
57
82
  return nil if @line_scanner.peek.nil?
58
83
 
59
- case @line_scanner.peek.tag # TODO: Use Null Pattern instead with a EndOfInput tag?
84
+ case @line_scanner.peek.tag
60
85
  when :list_item
61
86
  parse_list_item(indentation)
62
87
  when :dict_item, :key_item
@@ -74,27 +99,101 @@ module NestedText
74
99
  end
75
100
  end
76
101
 
102
+ def parse_list_item_value(indentation, value)
103
+ return value unless value.nil?
104
+
105
+ if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
106
+ parse_any(@line_scanner.peek.indentation)
107
+ elsif @line_scanner.peek.nil? || @line_scanner.peek.tag == :list_item
108
+ ''
109
+ end
110
+ end
111
+
112
+ def assert_list_line(line, indentation)
113
+ Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
114
+ raise Errors::ParseLineTypeExpectedListItemError, line unless line.tag == :list_item
115
+ raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
116
+ end
117
+
77
118
  def parse_list_item(indentation)
78
119
  result = []
79
120
  while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
80
121
  line = @line_scanner.read_next
122
+ assert_list_line(line, indentation)
123
+ result << parse_list_item_value(indentation, line.attribs['value'])
124
+ end
125
+ result
126
+ end
81
127
 
82
- Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
83
- raise Errors::ParseLineTypeExpectedListItemError, line unless line.tag == :list_item
84
- raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
85
-
86
- value = line.attribs["value"]
87
- if value.nil?
88
- if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
89
- value = parse_any(@line_scanner.peek.indentation)
90
- elsif @line_scanner.peek.nil? || @line_scanner.peek.tag == :list_item
91
- value = ""
92
- end
128
+ def deserialize_custom_class(hash, first_line)
129
+ return hash unless !@strict && hash.length == 2 && hash.key?(CUSTOM_CLASS_KEY)
130
+
131
+ class_name = hash[CUSTOM_CLASS_KEY]
132
+ begin
133
+ clazz = class_name == 'nil' ? NilClass : Object.const_get(class_name, false)
134
+ rescue NameError
135
+ raise Errors::ParseCustomClassNotFoundError.new(first_line, class_name)
136
+ end
137
+ raise Errors::ParseCustomClassNoCreateMethodError.new(first_line, class_name) unless clazz.respond_to? :nt_create
138
+
139
+ clazz.nt_create(hash['data'])
140
+ end
141
+
142
+ def parse_kv_dict_item(indentation, line)
143
+ key = line.attribs['key']
144
+ value = line.attribs['value']
145
+ if value.nil?
146
+ value = ''
147
+ if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
148
+ value = parse_any(@line_scanner.peek.indentation)
93
149
  end
150
+ end
151
+ [key, value]
152
+ end
94
153
 
95
- result << value
154
+ def parse_key_item_key(indentation, line)
155
+ key = line.attribs['key']
156
+ while @line_scanner.peek&.tag == :key_item && @line_scanner.peek.indentation == indentation
157
+ line = @line_scanner.read_next
158
+ key += "\n#{line.attribs['key']}"
96
159
  end
97
- result
160
+ key
161
+ end
162
+
163
+ def parse_key_item_value(indentation, line)
164
+ return '' if @line_scanner.peek.nil?
165
+
166
+ exp_types = %i[dict_item key_item list_item string_item]
167
+ unless exp_types.member?(@line_scanner.peek.tag)
168
+ raise Errors::ParseLineTypeNotExpectedError.new(line, exp_types, line.tag)
169
+ end
170
+
171
+ unless @line_scanner.peek.indentation > indentation
172
+ raise Errors::ParseMultilineKeyNoValueError,
173
+ line
174
+ end
175
+
176
+ parse_any(@line_scanner.peek.indentation)
177
+ end
178
+
179
+ def parse_kv_key_item(indentation, line)
180
+ key = parse_key_item_key(indentation, line)
181
+ value = parse_key_item_value(indentation, line)
182
+ [key, value]
183
+ end
184
+
185
+ def parse_dict_key_value(line, indentation)
186
+ if line.tag == :dict_item
187
+ parse_kv_dict_item(indentation, line)
188
+ else
189
+ parse_kv_key_item(indentation, line)
190
+ end
191
+ end
192
+
193
+ def assert_dict_item_line(line, indentation)
194
+ Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
195
+ raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
196
+ raise Errors::ParseLineTypeExpectedDictItemError, line unless %i[dict_item key_item].include? line.tag
98
197
  end
99
198
 
100
199
  def parse_dict_item(indentation)
@@ -103,170 +202,35 @@ module NestedText
103
202
  while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
104
203
  line = @line_scanner.read_next
105
204
  first_line = line if first_line.nil?
106
- Errors.raise_unrecognized_line(line) if line.tag == :unrecognized
107
- raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
108
- raise Errors::ParseLineTypeExpectedDictItemError, line unless %i[dict_item key_item].include? line.tag
109
-
110
- value = nil
111
- key = nil
112
- if line.tag == :dict_item
113
- key = line.attribs["key"]
114
- value = line.attribs["value"]
115
- if value.nil?
116
- value = ""
117
- if !@line_scanner.peek.nil? && @line_scanner.peek.indentation > indentation
118
- value = parse_any(@line_scanner.peek.indentation)
119
- end
120
- end
121
- else # :key_item
122
- key = line.attribs["key"]
123
- while @line_scanner.peek&.tag == :key_item && @line_scanner.peek.indentation == indentation
124
- line = @line_scanner.read_next
125
- key += "\n" + line.attribs["key"]
126
- end
127
- exp_types = %i[dict_item key_item list_item string_item]
128
- if @line_scanner.peek.nil?
129
- value = ""
130
- else
131
- unless exp_types.member?(@line_scanner.peek.tag)
132
- raise Errors::ParseLineTypeNotExpectedError.new(line, exp_types, line.tag)
133
- end
134
- raise Errors::ParseMultilineKeyNoValueError, line unless @line_scanner.peek.indentation > indentation
135
-
136
- value = parse_any(@line_scanner.peek.indentation)
137
- end
138
- end
205
+ assert_dict_item_line(line, indentation)
206
+ key, value = parse_dict_key_value(line, indentation)
139
207
  raise Errors::ParseDictDuplicateKeyError, line if result.key? key
140
208
 
141
209
  result[key] = value
142
210
  end
143
211
 
144
- # Custom class decoding.
145
- if !@strict && result.length == 2 && result.key?(CUSTOM_CLASS_KEY)
146
- class_name = result[CUSTOM_CLASS_KEY]
147
- begin
148
- clazz = class_name == "nil" ? NilClass : Object.const_get(class_name, false)
149
- rescue NameError
150
- raise Errors::ParseCustomClassNotFoundError.new(first_line, class_name)
151
- end
152
- if clazz.respond_to? :nt_create
153
- result = clazz.nt_create(result["data"])
154
- else
155
- raise Errors::ParseCustomClassNoCreateMethodError.new(first_line, class_name)
156
- end
157
- end
212
+ deserialize_custom_class(result, first_line)
213
+ end
158
214
 
159
- result
215
+ def assert_string_line(line, indentation)
216
+ raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
217
+ raise Errors::ParseLineTypeNotExpectedError.new(line, %i[string_item], line.tag) unless line.tag == :string_item
160
218
  end
161
219
 
162
220
  def parse_string_item(indentation)
163
221
  result = []
164
222
  while !@line_scanner.peek.nil? && @line_scanner.peek.indentation >= indentation
165
223
  line = @line_scanner.read_next
166
- raise Errors::ParseInvalidIndentationError.new(line, indentation) if line.indentation != indentation
167
- raise Errors::ParseLineTypeNotExpectedError.new(line, %i[string_item], line.tag) unless line.tag == :string_item
224
+ assert_string_line(line, indentation)
168
225
 
169
- value = line.attribs["value"]
226
+ value = line.attribs['value']
170
227
  result << value
171
228
  end
172
229
  result.join("\n")
173
230
  end
174
231
 
175
- def parse_inline_key
176
- key = []
177
- until @inline_scanner.empty? || [":", "{", "}", "[", "]", ","].include?(@inline_scanner.peek)
178
- key << @inline_scanner.read_next
179
- end
180
- if @inline_scanner.empty?
181
- raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
182
- @inline_scanner.pos)
183
- end
184
-
185
- last_char = @inline_scanner.read_next
186
- if last_char == "}" && key.empty?
187
- raise Errors::ParseInlineMissingValueError.new(@inline_scanner.line, @inline_scanner.pos - 1)
188
- end
189
- unless last_char == ":"
190
- raise Errors::ParseInlineDictKeySyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1, last_char)
191
- end
192
-
193
- key.join.strip
194
- end
195
-
196
- def parse_inline
197
- return nil if @inline_scanner.peek.nil?
198
-
199
- result = nil
200
- # Trim leading whitespaces
201
- @inline_scanner.read_next while !@inline_scanner.empty? && [" ", "\t"].include?(@inline_scanner.peek)
202
- case @inline_scanner.peek
203
- when "{"
204
- result = {}
205
- first = true
206
- loop do
207
- @inline_scanner.read_next
208
- break if first && @inline_scanner.peek == "}"
209
-
210
- first = false
211
- key = parse_inline_key
212
- value = parse_inline
213
- result[key] = value
214
- break unless @inline_scanner.peek == ","
215
- end
216
- if @inline_scanner.empty?
217
- raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
218
- @inline_scanner.pos)
219
- end
220
- last_char = @inline_scanner.read_next
221
- unless last_char == "}"
222
- raise Errors::ParseInlineDictSyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1,
223
- last_char)
224
- end
225
-
226
- when "["
227
- result = []
228
- first = true # TODO: can be replaced by checking result.empty? below?
229
- loop do
230
- @inline_scanner.read_next
231
- break if first && @inline_scanner.peek == "]"
232
-
233
- first = false
234
- result << parse_inline
235
- break unless @inline_scanner.peek == ","
236
- end
237
- if @inline_scanner.empty?
238
- raise Errors::ParseInlineNoClosingDelimiterError.new(@inline_scanner.line,
239
- @inline_scanner.pos)
240
- end
241
- last_char = @inline_scanner.read_next
242
-
243
- if last_char != "]"
244
- if result[-1] == ""
245
- raise Errors::ParseInlineMissingValueError.new(@inline_scanner.line, @inline_scanner.pos - 1)
246
- else
247
- raise Errors::ParseInlineListSyntaxError.new(@inline_scanner.line, @inline_scanner.pos - 1,
248
- last_char)
249
- end
250
- end
251
- else # Inline string
252
- inline_string = []
253
- until @inline_scanner.empty? || ["{", "}", "[", "]", ","].include?(@inline_scanner.peek)
254
- inline_string << @inline_scanner.read_next
255
- end
256
- result = inline_string.join.rstrip # Trim trailing whitespaces that lead up to next break point.
257
- end
258
- # Trim trailing whitespaces
259
- @inline_scanner.read_next while !@inline_scanner.empty? && [" ", "\t"].include?(@inline_scanner.peek)
260
- result
261
- end
262
-
263
232
  def parse_inline_dict
264
- @inline_scanner = InlineScanner.new(@line_scanner.read_next)
265
- result = parse_inline
266
- unless @inline_scanner.empty?
267
- raise Errors::ParseInlineExtraCharactersAfterDelimiterError.new(@inline_scanner.line, @inline_scanner.pos,
268
- @inline_scanner.remaining)
269
- end
233
+ result = InlineParser.new(@line_scanner.read_next).parse
270
234
  unless result.is_a? Hash
271
235
  raise Errors::AssertionError,
272
236
  "Expected inline value to be Hash but is #{result.class.name}"
@@ -276,12 +240,7 @@ module NestedText
276
240
  end
277
241
 
278
242
  def parse_inline_list
279
- @inline_scanner = InlineScanner.new(@line_scanner.read_next)
280
- result = parse_inline
281
- unless @inline_scanner.empty?
282
- raise Errors::ParseInlineExtraCharactersAfterDelimiterError.new(@inline_scanner.line, @inline_scanner.pos,
283
- @inline_scanner.remaining)
284
- end
243
+ result = InlineParser.new(@line_scanner.read_next).parse
285
244
  unless result.is_a? Array
286
245
  raise Errors::AssertionError,
287
246
  "Expected inline value to be Array but is #{result.class.name}"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nestedtext/errors_internal"
3
+ require 'nestedtext/errors_internal'
4
4
 
5
5
  module NestedText
6
6
  class LineScanner
@@ -83,7 +83,7 @@ module NestedText
83
83
  :inline_dict, # {key1: value1, key2: value2}
84
84
  :inline_list, # [value1, value2]
85
85
  :unrecognized # could not be determined
86
- ]
86
+ ].freeze
87
87
 
88
88
  attr_accessor :prev
89
89
  attr_reader :tag, :content, :indentation, :attribs, :lineno
@@ -112,12 +112,11 @@ module NestedText
112
112
  end
113
113
 
114
114
  def to_s
115
- "[##{@lineno}] #{" " * @indentation}#{@content}"
115
+ "[##{@lineno}] #{' ' * @indentation}#{@content}"
116
116
  end
117
117
 
118
118
  private
119
119
 
120
- # TODO: this regex must unit tested.
121
120
  PATTERN_DICT_ITEM = /^
122
121
  (?<key>[^\s].*?) # Key must start with a non-whitespace character, and goes until first
123
122
  \s*: # first optional space, or :-separator
@@ -127,37 +126,44 @@ module NestedText
127
126
  )?
128
127
  $/x
129
128
 
130
- def detect_line_tag_and_indentation
131
- @indentation += 1 while @indentation < @content.length && @content[@indentation] == " "
129
+ def fast_forward_indentation
130
+ @indentation += 1 while @indentation < @content.length && @content[@indentation] == ' '
132
131
  @content = @content[@indentation..]
132
+ end
133
133
 
134
- if @content.length == 0
134
+ def detect_line_tag
135
+ if @content.length.zero?
135
136
  self.tag = :blank
136
- elsif @content[0] == "#"
137
+ elsif @content[0] == '#'
137
138
  self.tag = :comment
138
139
  elsif @content =~ /^:(?: |$)/
139
140
  self.tag = :key_item
140
- @attribs["key"] = @content[2..] || ""
141
+ @attribs['key'] = @content[2..] || ''
141
142
  elsif @content =~ /^-(?: |$)/
142
143
  self.tag = :list_item
143
- @attribs["value"] = @content[2..]
144
+ @attribs['value'] = @content[2..]
144
145
  elsif @content =~ /^>(?: |$)/
145
146
  self.tag = :string_item
146
- @attribs["value"] = @content[2..] || ""
147
- elsif @content[0] == "{"
147
+ @attribs['value'] = @content[2..] || ''
148
+ elsif @content[0] == '{'
148
149
  self.tag = :inline_dict
149
- elsif @content[0] == "["
150
+ elsif @content[0] == '['
150
151
  self.tag = :inline_list
151
152
  elsif @content =~ PATTERN_DICT_ITEM
152
153
  self.tag = :dict_item
153
- @attribs["key"] = Regexp.last_match(:key)
154
- @attribs["value"] = Regexp.last_match(:value)
154
+ @attribs['key'] = Regexp.last_match(:key)
155
+ @attribs['value'] = Regexp.last_match(:value)
155
156
  else
156
157
  # Don't raise error here, as this line might not have been consumed yet,
157
158
  # thus could hide an error that we detect when parsing the previous line.
158
159
  self.tag = :unrecognized
159
160
  end
160
161
  end
162
+
163
+ def detect_line_tag_and_indentation
164
+ fast_forward_indentation
165
+ detect_line_tag
166
+ end
161
167
  end
162
168
  private_constant :Line
163
169
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NestedText
4
4
  # The version of this library.
5
- VERSION = "3.2.1"
5
+ VERSION = '4.0.0'
6
6
  end
data/lib/nestedtext.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "nestedtext/core_ext"
4
- require_relative "nestedtext/decode"
5
- require_relative "nestedtext/encode"
6
- require_relative "nestedtext/encode_helpers"
7
- require_relative "nestedtext/error"
8
- require_relative "nestedtext/version"
3
+ require_relative 'nestedtext/core_ext'
4
+ require_relative 'nestedtext/decode'
5
+ require_relative 'nestedtext/encode'
6
+ require_relative 'nestedtext/encode_helpers'
7
+ require_relative 'nestedtext/error'
8
+ require_relative 'nestedtext/version'
9
9
 
10
10
  ##
11
11
  # # NestedText
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nestedtext
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Westrup
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-27 00:00:00.000000000 Z
11
+ date: 2022-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: warning
@@ -64,6 +64,7 @@ files:
64
64
  - lib/nestedtext/encode_helpers.rb
65
65
  - lib/nestedtext/error.rb
66
66
  - lib/nestedtext/errors_internal.rb
67
+ - lib/nestedtext/inline_parser.rb
67
68
  - lib/nestedtext/parser.rb
68
69
  - lib/nestedtext/scanners.rb
69
70
  - lib/nestedtext/version.rb