parsanol 1.3.13-arm-linux → 1.3.15-arm-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27b35691df09d839dc38f8cfc72e117b78faedb2e6674171700f89333123e60c
4
- data.tar.gz: f76f648e77c782eb676651b1cbdec289333fe56dbac8e26b5ef74572dccaf347
3
+ metadata.gz: e98e448bc4374451a62e175387759b4db66c90160e5df3204e652c17a751ddd7
4
+ data.tar.gz: 7f081c2cd9e6fac46cd4e43d15c91a6755af3aad8b01c35b5cb542d190cf2786
5
5
  SHA512:
6
- metadata.gz: 1e47cf501b7c1a34422aa2a444ab2baf8b32562eae3cc810329547c8b7fea306b4d4dcb3e3615cee78877b9104594541956615995af9aed1f2951bda68272428
7
- data.tar.gz: 7d6558cf5ec6a243b06bb1fb0c4b863863d4ebe09890cc475c0d873941426363ba45ed1f3617f5bc75b88e1c96599b04d5e4acd3abb249a8618317726dd57330
6
+ metadata.gz: 682268466119952e99a678a79270d6fe69a449630665a8e082e7f01f709903f6190552053e6328ec74f47fd452d1b946bedd5b89687a20594debbe33a6e9a2d2
7
+ data.tar.gz: 386e99e3a11788991e7b1fc5fb0ae55a6c95caa0b60508c62c3f997492c213887135d9d7b4bcb04a72b7d9d8c8bd20a9b20b105b6a2e31cc78b814db32380306
Binary file
Binary file
Binary file
Binary file
@@ -30,9 +30,34 @@ module Parsanol
30
30
  # @return [Object] the parsed result
31
31
  # @raise [Parsanol::ParseFailed] on parse failure
32
32
  def parse(source, options = {})
33
- input = normalize_input(source)
34
33
  must_consume_all = !options[:prefix]
35
34
 
35
+ # Compiled-VM fast path: String input, full-consumption semantics.
36
+ # On failure (or unsupported grammar / budget exhaustion) fall
37
+ # through to the interpreter, which also produces the exact
38
+ # cause-tree diagnostics.
39
+ if must_consume_all && source.is_a?(String) &&
40
+ (program = VM.program_for(self))
41
+ result = VM.run(program, source, true)
42
+ if result == VM::BAIL
43
+ # Internal bail: fall back to the interpreter and skip the VM
44
+ # for this grammar from now on.
45
+ VM.disable_for!(self)
46
+ elsif result.first == :heavy
47
+ # Succeeded but burned >1/8 of the step budget —
48
+ # heavy-backtracking grammar; the memoizing interpreter is
49
+ # the better engine from now on.
50
+ VM.disable_for!(self)
51
+ return finalize_result(result[1])
52
+ elsif result.first
53
+ return finalize_result(result[1])
54
+ end
55
+ # Clean [false, nil]: the input does not parse — the interpreter
56
+ # reparse below produces the detailed cause tree.
57
+ end
58
+
59
+ input = normalize_input(source)
60
+
36
61
  # Initial parse attempt (no error collection)
37
62
  success, value = run_with_context(input, nil, must_consume_all)
38
63
  return finalize_result(value) if success
@@ -65,18 +65,25 @@ module Parsanol
65
65
  raise "BUG: Unknown tag #{tag.inspect}."
66
66
  end
67
67
 
68
- # Lisp style fold left where the first element builds the basis for
69
- # an inject. Optimized with early return and reduced method calls.
68
+ # Lisp style fold left where the first non-nil element builds the
69
+ # basis for an inject. Nil entries are skipped in a single pass so
70
+ # callers do not need an intermediate #compact.
70
71
  #
71
72
  def foldl(list)
72
73
  len = list.size
73
- return "" if len.zero?
74
- return list[0] if len == 1 # Fast path for single element
74
+ i = 0
75
+ while i < len && list[i].nil?
76
+ i += 1
77
+ end
78
+ return "" if i >= len
79
+
80
+ result = list[i]
81
+ i += 1
82
+ return result if i >= len # Fast path for single non-nil element
75
83
 
76
- result = list[0]
77
- i = 1
78
84
  while i < len
79
- result = yield(result, list[i])
85
+ e = list[i]
86
+ result = yield(result, e) unless e.nil?
80
87
  i += 1
81
88
  end
82
89
  result
@@ -87,7 +94,7 @@ module Parsanol
87
94
  # @api private
88
95
  #
89
96
  def flatten_sequence(list)
90
- foldl(list.compact) do |r, e| # and then merge flat elements
97
+ foldl(list) do |r, e|
91
98
  merge_fold(r, e)
92
99
  end
93
100
  end
@@ -161,22 +168,65 @@ module Parsanol
161
168
  if has_hash
162
169
  # If keyed subtrees are in the array, we'll want to discard all
163
170
  # strings inbetween. To keep them, name them.
164
- return list.select { |e| e.instance_of?(Hash) }
171
+ hashes = []
172
+ i = 0
173
+ while i < len
174
+ e = list[i]
175
+ hashes << e if e.instance_of?(Hash)
176
+ i += 1
177
+ end
178
+ return hashes
165
179
  end
166
180
 
167
181
  if has_array
168
182
  # If any arrays are nested in this array, flatten all arrays to this
169
183
  # level.
170
- return list
171
- .select { |e| e.instance_of?(Array) }
172
- .flatten(1)
184
+ flat = []
185
+ i = 0
186
+ while i < len
187
+ e = list[i]
188
+ if e.instance_of?(Array)
189
+ flat.concat(e)
190
+ end
191
+ i += 1
192
+ end
193
+ return flat
173
194
  end
174
195
 
175
196
  # Consistent handling of empty lists, when we act on a named result
176
197
  return [] if named && list.empty?
177
198
 
178
- # If there are only strings, concatenate them and return that.
179
- foldl(list.compact) { |s, e| s + e }
199
+ # All-string (or Slice) repetition: join once instead of O(n)
200
+ # intermediate Slice allocations via repeated #+.
201
+ join_string_list(list)
202
+ end
203
+
204
+ # Join a list of string-like values into a single Slice (or String),
205
+ # preserving the first Slice's offset and input. Nils are skipped.
206
+ def join_string_list(list)
207
+ first_slice = nil
208
+ content = nil
209
+ i = 0
210
+ len = list.size
211
+ while i < len
212
+ e = list[i]
213
+ unless e.nil?
214
+ if first_slice.nil? && e.instance_of?(Parsanol::Slice)
215
+ first_slice = e
216
+ end
217
+ piece = e.instance_of?(Parsanol::Slice) ? e.content : e.to_s
218
+ if content.nil?
219
+ content = +piece
220
+ else
221
+ content << piece
222
+ end
223
+ end
224
+ i += 1
225
+ end
226
+ return "" if content.nil?
227
+ return first_slice.class.new(first_slice.offset, content, first_slice.input) if first_slice
228
+
229
+ content
180
230
  end
181
231
 
182
232
  # That annoying warning 'Duplicate subtrees while merging result' comes
@@ -171,16 +171,15 @@ module Parsanol
171
171
  context.err_at(self, source, @min_error, source.bytepos, [v3])
172
172
  end
173
173
 
174
- # General repetition with buffer pooling
174
+ # General repetition: builds the tagged result Array in place so
175
+ # finalize_result can flatten without an intermediate Buffer/LazyResult
176
+ # materialization.
175
177
  def try_general(source, context, consume_all)
176
178
  start_pos = source.bytepos
177
179
  occurrence = 0
178
-
179
- # Estimate buffer size
180
- estimate = [@max || 10, 10].min
181
- buffer = context.acquire_buffer(size: estimate + 1)
182
- buffer.push(@result_tag)
183
-
180
+ # Pre-size for the tag + a modest run; Array growth is amortized.
181
+ result = Array.new([@max || 8, 8].min + 1)
182
+ result[0] = @result_tag
184
183
  last_error = nil
185
184
 
186
185
  loop do
@@ -190,14 +189,13 @@ module Parsanol
190
189
  break unless success
191
190
 
192
191
  occurrence += 1
193
- buffer.push(value)
192
+ result[occurrence] = value
194
193
 
195
194
  break if @max && occurrence >= @max
196
195
  end
197
196
 
198
197
  # Check minimum bound
199
198
  if occurrence < @min
200
- context.release_buffer(buffer)
201
199
  source.bytepos = start_pos
202
200
  return context.err_at(self, source, @min_error, start_pos,
203
201
  [last_error])
@@ -205,11 +203,12 @@ module Parsanol
205
203
 
206
204
  # Check complete consumption
207
205
  if consume_all && source.chars_left.positive?
208
- context.release_buffer(buffer)
209
206
  return context.err(self, source, @extra_error, [last_error])
210
207
  end
211
208
 
212
- ok(Parsanol::LazyResult.new(buffer, context))
209
+ # Trim to the actual filled length (tag + occurrence matches).
210
+ result.pop(result.size - occurrence - 1) if result.size > occurrence + 1
211
+ ok(result)
213
212
  end
214
213
 
215
214
  # Tree memoization for GPEG-style caching
@@ -227,23 +226,21 @@ module Parsanol
227
226
 
228
227
  # Parse and cache
229
228
  occurrence = 0
230
- estimate = [@max || 10, 10].min
231
- buffer = context.acquire_buffer(size: estimate + 1)
232
- buffer.push(@result_tag)
229
+ result = Array.new([@max || 8, 8].min + 1)
230
+ result[0] = @result_tag
233
231
 
234
232
  positions = context.acquire_array
235
233
  positions << start_pos
236
234
  last_error = nil
237
235
 
238
236
  loop do
239
- source.bytepos
240
237
  success, value = @parslet.apply(source, context, false)
241
238
  last_error = value
242
239
 
243
240
  break unless success
244
241
 
245
242
  occurrence += 1
246
- buffer.push(value)
243
+ result[occurrence] = value
247
244
  positions << source.bytepos
248
245
 
249
246
  break if @max && occurrence >= @max
@@ -252,13 +249,13 @@ module Parsanol
252
249
  # Cache successful prefix
253
250
  if occurrence.positive?
254
251
  end_pos = positions[occurrence]
255
- context.store_tree_memo(cache_key, start_pos, buffer.to_a[1..],
256
- end_pos)
252
+ context.store_tree_memo(cache_key, start_pos,
253
+ result[1, occurrence], end_pos)
257
254
  end
255
+ context.release_array(positions) if context.respond_to?(:release_array)
258
256
 
259
257
  # Check minimum
260
258
  if occurrence < @min
261
- context.release_buffer(buffer)
262
259
  source.bytepos = start_pos
263
260
  return context.err_at(self, source, @min_error, start_pos,
264
261
  [last_error])
@@ -266,11 +263,11 @@ module Parsanol
266
263
 
267
264
  # Check consumption
268
265
  if consume_all && source.chars_left.positive?
269
- context.release_buffer(buffer)
270
266
  return context.err(self, source, @extra_error, [last_error])
271
267
  end
272
268
 
273
- ok(Parsanol::LazyResult.new(buffer, context))
269
+ result.pop(result.size - occurrence - 1) if result.size > occurrence + 1
270
+ ok(result)
274
271
  end
275
272
  end
276
273
  end
@@ -105,7 +105,7 @@ module Parsanol
105
105
  ok([:sequence, value])
106
106
  end
107
107
 
108
- # Two-element sequence with buffer pooling
108
+ # Two-element sequence
109
109
  def match_pair(p1, p2, source, context, consume_all)
110
110
  success, v1 = p1.apply(source, context, false)
111
111
  return context.err(self, source, @fail_msg, [v1]) unless success
@@ -113,14 +113,10 @@ module Parsanol
113
113
  success, v2 = p2.apply(source, context, consume_all)
114
114
  return context.err(self, source, @fail_msg, [v2]) unless success
115
115
 
116
- buffer = context.acquire_buffer(size: 3)
117
- buffer.push(:sequence)
118
- buffer.push(v1)
119
- buffer.push(v2)
120
- ok(Parsanol::LazyResult.new(buffer, context))
116
+ ok([:sequence, v1, v2])
121
117
  end
122
118
 
123
- # Three-element sequence with buffer pooling
119
+ # Three-element sequence
124
120
  def match_triple(p1, p2, p3, source, context, consume_all)
125
121
  success, v1 = p1.apply(source, context, false)
126
122
  return context.err(self, source, @fail_msg, [v1]) unless success
@@ -131,20 +127,16 @@ module Parsanol
131
127
  success, v3 = p3.apply(source, context, consume_all)
132
128
  return context.err(self, source, @fail_msg, [v3]) unless success
133
129
 
134
- buffer = context.acquire_buffer(size: 4)
135
- buffer.push(:sequence)
136
- buffer.push(v1)
137
- buffer.push(v2)
138
- buffer.push(v3)
139
- ok(Parsanol::LazyResult.new(buffer, context))
130
+ ok([:sequence, v1, v2, v3])
140
131
  end
141
132
 
142
133
  # General case for N elements
143
134
  def match_general(components, source, context, consume_all)
144
- buffer = context.acquire_buffer(size: components.size + 1)
145
- buffer.push(:sequence)
135
+ n = components.size
136
+ result = Array.new(n + 1)
137
+ result[0] = :sequence
146
138
 
147
- last_idx = components.size - 1
139
+ last_idx = n - 1
148
140
  idx = 0
149
141
 
150
142
  while idx <= last_idx
@@ -152,15 +144,14 @@ module Parsanol
152
144
  success, value = components[idx].apply(source, context, must_consume)
153
145
 
154
146
  unless success
155
- context.release_buffer(buffer)
156
147
  return context.err(self, source, @fail_msg, [value])
157
148
  end
158
149
 
159
- buffer.push(value)
150
+ result[idx + 1] = value
160
151
  idx += 1
161
152
  end
162
153
 
163
- ok(Parsanol::LazyResult.new(buffer, context))
154
+ ok(result)
164
155
  end
165
156
 
166
157
  # Merges adjacent string atoms using Rope for efficiency
@@ -8,6 +8,9 @@ module Parsanol
8
8
  module Parser
9
9
  GRAMMAR_HASH_CACHE = Hash.new
10
10
  GRAMMAR_CACHE = Hash.new
11
+ # structure-hash => Rust-side grammar handle. Keyed by content so a
12
+ # recycled object_id can never alias a different grammar.
13
+ HANDLE_CACHE = Hash.new
11
14
 
12
15
  class << self
13
16
  @cached_available = nil
@@ -40,26 +43,51 @@ module Parsanol
40
43
 
41
44
  # Serialize a Ruby grammar to JSON (cached).
42
45
  def serialize_grammar(root_atom)
43
- root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
44
- obj_id = root_atom.object_id
45
- cache_key = GRAMMAR_HASH_CACHE[obj_id] ||= grammar_structure_hash(root_atom)
46
- GRAMMAR_CACHE[cache_key] ||= GrammarSerializer.serialize(root_atom)
46
+ grammar_json(root_atom)
47
+ end
48
+
49
+ # Resolve a Rust-side handle for the grammar, registering it once.
50
+ # Per-call cost is the memoized structure-hash lookup plus one Hash
51
+ # access — no JSON marshal, no re-hash inside Rust.
52
+ def grammar_handle(root_atom)
53
+ cache_key = grammar_cache_key(root_atom)
54
+ HANDLE_CACHE[cache_key] ||= Native._register_grammar(grammar_json(root_atom))
55
+ end
56
+
57
+ # Drop a handle whose Rust-side entry no longer exists; the next
58
+ # grammar_handle call re-registers.
59
+ def invalidate_handle(handle)
60
+ HANDLE_CACHE.reject! { |_key, cached| cached == handle }
47
61
  end
48
62
 
49
63
  def clear_cache
50
64
  GRAMMAR_HASH_CACHE.clear
51
65
  GRAMMAR_CACHE.clear
66
+ HANDLE_CACHE.clear
52
67
  end
53
68
 
54
69
  def cache_stats
55
70
  {
56
71
  hash_cache_size: GRAMMAR_HASH_CACHE.size,
57
72
  grammar_cache_size: GRAMMAR_CACHE.size,
73
+ handle_cache_size: HANDLE_CACHE.size,
58
74
  }
59
75
  end
60
76
 
61
77
  private
62
78
 
79
+ def grammar_cache_key(root_atom)
80
+ root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
81
+ obj_id = root_atom.object_id
82
+ GRAMMAR_HASH_CACHE[obj_id] ||= grammar_structure_hash(root_atom)
83
+ end
84
+
85
+ def grammar_json(root_atom)
86
+ root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
87
+ cache_key = grammar_cache_key(root_atom)
88
+ GRAMMAR_CACHE[cache_key] ||= GrammarSerializer.serialize(root_atom)
89
+ end
90
+
63
91
  def grammar_structure_hash(atom)
64
92
  Digest::MD5.hexdigest(atom_structure(atom).to_s)
65
93
  end
@@ -132,9 +132,11 @@ module Parsanol
132
132
  # Transform the value
133
133
  transformed = transform(value, named: true)
134
134
 
135
- # Check if value is a tagged repetition from native parser
135
+ # Check if value is a tagged repetition from native parser.
136
+ # The Rust handle path tags with Symbols, the batch decoder with
137
+ # ":repetition" Strings — accept both.
136
138
  is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
137
- value.first.is_a?(String) && value.first == REPETITION_TAG
139
+ [REPETITION_SYM, REPETITION_TAG].include?(value.first)
138
140
 
139
141
  # Check RAW value for repetition pattern BEFORE transformation
140
142
  # Array with items that all have the parent key
@@ -203,13 +205,12 @@ module Parsanol
203
205
  # Empty array from repetition stays as empty array
204
206
  if transformed.empty?
205
207
  { sym_key => EMPTY_ARRAY }
206
- # Check if items already have the same key (avoid double-wrapping)
207
- elsif transformed.all? do |item|
208
- item.is_a?(Hash) && item.key?(sym_key)
209
- end
208
+ # Hash items already carry their own capture names — a
209
+ # repetition of named captures keeps them as-is (parslet
210
+ # semantics); only unnamed items get the parent name per item.
211
+ elsif transformed.all?(Hash)
210
212
  { sym_key => transformed }
211
213
  else
212
- # Wrap each item with the name
213
214
  { sym_key => transformed.map { |item| { sym_key => item } } }
214
215
  end
215
216
  elsif transformed.is_a?(::Parsanol::Slice) && transformed.empty?
@@ -38,22 +38,14 @@ module Parsanol
38
38
  def parse(grammar, input)
39
39
  raise LoadError, "Native parser not available" unless available?
40
40
 
41
- # Handle both grammar atoms and pre-serialized JSON strings
42
- grammar_json = if grammar.is_a?(String)
43
- grammar
44
- else
45
- Parser.serialize_grammar(grammar)
46
- end
47
-
48
- # Use _parse_raw which returns properly tagged Ruby arrays via transform_ast.
49
- # The batch format doesn't preserve :repetition/:sequence tags, so we use
50
- # the direct FFI path. Apply the Ruby transformer to handle tags correctly.
51
- begin
52
- raw_ast = _parse_raw(grammar_json, input)
53
- rescue RuntimeError => e
54
- raise_native_parse_error(e, grammar, input)
41
+ # Both sub-methods return the final decoded tree; on native failure
42
+ # they fall back to the pure-Ruby parser, whose result is already
43
+ # final and must not be transformed again.
44
+ if grammar.is_a?(String)
45
+ parse_json_grammar(grammar, input)
46
+ else
47
+ parse_atom_grammar(grammar, input)
55
48
  end
56
- BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
57
49
  end
58
50
 
59
51
  # Memory-bounded parsing without packrat cache.
@@ -73,12 +65,14 @@ module Parsanol
73
65
  Parser.serialize_grammar(grammar)
74
66
  end
75
67
 
68
+ # Decode here, not around the fallback: raise_native_parse_error
69
+ # returns the Ruby parser's already-final tree on success.
76
70
  begin
77
- raw_ast = _parse_fresh_raw(grammar_json, input)
71
+ BatchDecoder.decode_and_flatten(_parse_fresh_raw(grammar_json, input),
72
+ input, Parsanol::Slice)
78
73
  rescue RuntimeError => e
79
74
  raise_native_parse_error(e, grammar, input)
80
75
  end
81
- BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
82
76
  end
83
77
 
84
78
  # Parse and return RAW AST without transformation.
@@ -206,6 +200,38 @@ module Parsanol
206
200
  cause = Parsanol::Cause.new(error.message, source, source.bytepos)
207
201
  raise Parsanol::ParseFailed.new(cause.to_s, cause)
208
202
  end
203
+
204
+ # Pre-serialized JSON grammar path (library authors with cached JSON).
205
+ def parse_json_grammar(grammar_json, input)
206
+ BatchDecoder.decode_and_flatten(_parse_raw(grammar_json, input),
207
+ input, Parsanol::Slice)
208
+ rescue RuntimeError => e
209
+ raise_native_parse_error(e, grammar_json, input)
210
+ end
211
+
212
+ # Grammar-atom path: registers once and parses by Rust-side handle,
213
+ # so steady-state calls marshal no JSON and copy no input string.
214
+ def parse_atom_grammar(grammar, input)
215
+ handle = Parser.grammar_handle(grammar)
216
+
217
+ begin
218
+ BatchDecoder.decode_and_flatten(_parse_handle(handle, input),
219
+ input, Parsanol::Slice)
220
+ rescue ArgumentError
221
+ # Handle dropped Rust-side (e.g. cache cleared): re-register once.
222
+ Parser.invalidate_handle(handle)
223
+ begin
224
+ BatchDecoder.decode_and_flatten(
225
+ _parse_handle(Parser.grammar_handle(grammar), input),
226
+ input, Parsanol::Slice
227
+ )
228
+ rescue RuntimeError => e
229
+ raise_native_parse_error(e, grammar, input)
230
+ end
231
+ rescue RuntimeError => e
232
+ raise_native_parse_error(e, grammar, input)
233
+ end
234
+ end
209
235
  end
210
236
  end
211
237
  end
@@ -109,32 +109,36 @@ module Parsanol
109
109
  # result[:name].to_s # => "hello"
110
110
  #
111
111
  def parse(input, mode_or_opts = {}, **kwargs)
112
- if mode_or_opts.is_a?(Hash) && !kwargs.key?(:mode)
113
- # Legacy API: parse(input, options={})
114
- merged = mode_or_opts.merge(kwargs)
115
- if Parsanol::Native.available? && !merged.key?(:prefix) &&
116
- !merged.key?(:reporter)
117
- # Native backend by default; falls back to pure Ruby inside
118
- # parse_native when the extension is missing.
119
- parse_native(input, merged)
112
+ opts =
113
+ if mode_or_opts.is_a?(Hash)
114
+ mode_or_opts.merge(kwargs)
115
+ elsif mode_or_opts.nil?
116
+ kwargs
120
117
  else
121
- super(input, merged)
118
+ # New API: parse(input, :mode, **options)
119
+ kwargs[:mode] = mode_or_opts
120
+ kwargs
122
121
  end
122
+ # parse(input, {mode: :ruby, ...}) is the same call shape a subclass
123
+ # forwarding options via super produces; :mode must be honored
124
+ # wherever it appears, or those callers silently get the native path.
125
+ mode = opts.delete(:mode) ||
126
+ (if Parsanol::Native.available? && !opts.key?(:prefix) &&
127
+ !opts.key?(:reporter)
128
+ :native
129
+ else
130
+ :ruby
131
+ end)
132
+ case mode
133
+ when :ruby
134
+ super(input, opts)
135
+ when :native
136
+ parse_native(input, opts)
137
+ when :json
138
+ parse_json(input, opts)
123
139
  else
124
- # New API: parse(input, mode:, **options)
125
- mode = kwargs.delete(:mode) ||
126
- (Parsanol::Native.available? ? :native : :ruby)
127
- case mode
128
- when :ruby
129
- super(input, kwargs)
130
- when :native
131
- parse_native(input, kwargs)
132
- when :json
133
- parse_json(input, kwargs)
134
- else
135
- raise ArgumentError,
136
- "Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
137
- end
140
+ raise ArgumentError,
141
+ "Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
138
142
  end
139
143
  end
140
144
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.13"
4
+ VERSION = "1.3.15"
5
5
  end