parsanol 1.3.5-aarch64-linux → 1.3.7-aarch64-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 +4 -4
- data/Rakefile +48 -48
- data/lib/parsanol/ast_visitor.rb +1 -1
- data/lib/parsanol/atoms/alternative.rb +3 -2
- data/lib/parsanol/atoms/base.rb +12 -6
- data/lib/parsanol/atoms/can_flatten.rb +8 -8
- data/lib/parsanol/atoms/context.rb +23 -16
- data/lib/parsanol/atoms/custom.rb +2 -2
- data/lib/parsanol/atoms/dynamic.rb +1 -1
- data/lib/parsanol/atoms/infix.rb +10 -5
- data/lib/parsanol/atoms/lookahead.rb +7 -4
- data/lib/parsanol/atoms/re.rb +1 -1
- data/lib/parsanol/atoms/repetition.rb +29 -11
- data/lib/parsanol/atoms/sequence.rb +3 -2
- data/lib/parsanol/atoms/str.rb +9 -3
- data/lib/parsanol/atoms.rb +20 -20
- data/lib/parsanol/builder_callbacks.rb +2 -2
- data/lib/parsanol/cause.rb +2 -2
- data/lib/parsanol/context.rb +2 -2
- data/lib/parsanol/error_reporter.rb +5 -5
- data/lib/parsanol/expression/treetop.rb +17 -17
- data/lib/parsanol/expression.rb +1 -1
- data/lib/parsanol/fast_mode.rb +50 -12
- data/lib/parsanol/first_set.rb +1 -1
- data/lib/parsanol/grammar_builder.rb +10 -8
- data/lib/parsanol/incremental_parser.rb +13 -8
- data/lib/parsanol/interval_tree.rb +12 -3
- data/lib/parsanol/lazy_result.rb +2 -2
- data/lib/parsanol/mermaid.rb +12 -9
- data/lib/parsanol/native/batch_decoder.rb +13 -9
- data/lib/parsanol/native/dynamic.rb +7 -6
- data/lib/parsanol/native/parser.rb +7 -5
- data/lib/parsanol/native/serializer.rb +42 -42
- data/lib/parsanol/native/transformer.rb +55 -28
- data/lib/parsanol/native/types.rb +3 -3
- data/lib/parsanol/native.rb +26 -20
- data/lib/parsanol/optimizer.rb +6 -6
- data/lib/parsanol/optimizers/choice_optimizer.rb +1 -1
- data/lib/parsanol/optimizers/cut_inserter.rb +5 -2
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +9 -3
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +5 -5
- data/lib/parsanol/optimizers/sequence_optimizer.rb +1 -1
- data/lib/parsanol/options/zero_copy.rb +1 -1
- data/lib/parsanol/options.rb +1 -1
- data/lib/parsanol/parallel.rb +4 -3
- data/lib/parsanol/parser.rb +18 -16
- data/lib/parsanol/parslet.rb +7 -7
- data/lib/parsanol/pattern/binding.rb +1 -1
- data/lib/parsanol/pattern.rb +4 -1
- data/lib/parsanol/pool.rb +3 -3
- data/lib/parsanol/pools/buffer_pool.rb +2 -2
- data/lib/parsanol/pools/position_pool.rb +2 -2
- data/lib/parsanol/position.rb +1 -1
- data/lib/parsanol/result_builder.rb +4 -4
- data/lib/parsanol/result_stream.rb +10 -5
- data/lib/parsanol/slice.rb +11 -8
- data/lib/parsanol/source.rb +14 -9
- data/lib/parsanol/source_location.rb +1 -1
- data/lib/parsanol/streaming_parser.rb +3 -3
- data/lib/parsanol/string_view.rb +4 -1
- data/lib/parsanol/transform.rb +2 -2
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/wasm_parser.rb +1 -1
- data/lib/parsanol.rb +37 -39
- data/parsanol.gemspec +30 -30
- metadata +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "parsanol/native/transformer"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Native
|
|
@@ -59,7 +59,7 @@ module Parsanol
|
|
|
59
59
|
# @param slice_class [Class] The Slice class to use
|
|
60
60
|
# @param grammar_atom [Parsanol::Atoms::Base] The grammar atom (unused, kept for API compat)
|
|
61
61
|
# @return [Object] Transformed Ruby AST
|
|
62
|
-
def decode_and_flatten(data, input, slice_class,
|
|
62
|
+
def decode_and_flatten(data, input, slice_class, _grammar_atom)
|
|
63
63
|
# Check if data is batch data (flat u64 array) or already a Ruby value
|
|
64
64
|
if data.is_a?(Integer) || (data.is_a?(Array) && data.first.is_a?(Integer))
|
|
65
65
|
# Batch data (flat u64 array) - decode first, then transform
|
|
@@ -84,11 +84,13 @@ module Parsanol
|
|
|
84
84
|
case value
|
|
85
85
|
when Array
|
|
86
86
|
# Recursively process array elements
|
|
87
|
-
processed = value.map
|
|
87
|
+
processed = value.map do |v|
|
|
88
|
+
join_consecutive_slices(v, slice_class, input)
|
|
89
|
+
end
|
|
88
90
|
|
|
89
91
|
# Check if all non-nil elements are Slices
|
|
90
92
|
non_nil = processed.compact
|
|
91
|
-
if non_nil.all?
|
|
93
|
+
if non_nil.all?(slice_class)
|
|
92
94
|
# Check if slices are consecutive
|
|
93
95
|
if slices_consecutive?(non_nil)
|
|
94
96
|
# Join into single slice
|
|
@@ -129,7 +131,7 @@ module Parsanol
|
|
|
129
131
|
last = slices.last
|
|
130
132
|
total_length = last.offset + last.content.bytesize - first.offset
|
|
131
133
|
content = input_bytes[first.offset, total_length]
|
|
132
|
-
content = content.force_encoding(
|
|
134
|
+
content = content.force_encoding("UTF-8") if content
|
|
133
135
|
slice_class.new(first.offset, content, input)
|
|
134
136
|
end
|
|
135
137
|
|
|
@@ -156,7 +158,7 @@ module Parsanol
|
|
|
156
158
|
bits = @data[@pos]
|
|
157
159
|
@pos += 1
|
|
158
160
|
# Convert IEEE 754 bits to float
|
|
159
|
-
[bits].pack(
|
|
161
|
+
[bits].pack("Q").unpack1("D")
|
|
160
162
|
when TAG_STRING
|
|
161
163
|
offset = @data[@pos]
|
|
162
164
|
length = @data[@pos + 1]
|
|
@@ -203,6 +205,7 @@ module Parsanol
|
|
|
203
205
|
|
|
204
206
|
# Read key
|
|
205
207
|
raise "Expected TAG_HASH_KEY, got #{tag}" unless tag == TAG_HASH_KEY
|
|
208
|
+
|
|
206
209
|
@pos += 1
|
|
207
210
|
key = decode_inline_string
|
|
208
211
|
|
|
@@ -228,22 +231,23 @@ module Parsanol
|
|
|
228
231
|
def decode_inline_string_bytes(len)
|
|
229
232
|
# Read u64 chunks
|
|
230
233
|
chunks = (len + 7) / 8
|
|
231
|
-
bytes = String.new(encoding:
|
|
234
|
+
bytes = String.new(encoding: "ASCII-8BIT", capacity: len)
|
|
232
235
|
chunks.times do
|
|
233
236
|
chunk = @data[@pos]
|
|
234
237
|
@pos += 1
|
|
235
238
|
8.times do |byte_idx|
|
|
236
239
|
break if bytes.bytesize >= len
|
|
240
|
+
|
|
237
241
|
bytes << ((chunk >> (byte_idx * 8)) & 0xFF)
|
|
238
242
|
end
|
|
239
243
|
end
|
|
240
244
|
|
|
241
|
-
bytes.force_encoding(
|
|
245
|
+
bytes.force_encoding("UTF-8")
|
|
242
246
|
end
|
|
243
247
|
|
|
244
248
|
def create_slice(offset, length)
|
|
245
249
|
content = @input_bytes[offset, length]
|
|
246
|
-
content = content.force_encoding(
|
|
250
|
+
content = content.force_encoding("UTF-8") if content
|
|
247
251
|
@slice_class.new(offset, content, @input)
|
|
248
252
|
end
|
|
249
253
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "json"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Native
|
|
@@ -49,13 +49,14 @@ module Parsanol
|
|
|
49
49
|
#
|
|
50
50
|
def register(block, description: nil)
|
|
51
51
|
# Register with Rust FFI
|
|
52
|
-
ffi_id = Native.register_callback(@next_id,
|
|
52
|
+
ffi_id = Native.register_callback(@next_id,
|
|
53
|
+
description || "Ruby callback ##{@next_id}")
|
|
53
54
|
|
|
54
55
|
# Also keep a Ruby-side reference for GC safety
|
|
55
56
|
@mutex.synchronize do
|
|
56
57
|
@callbacks[ffi_id] = {
|
|
57
58
|
block: block,
|
|
58
|
-
description: description || "Ruby callback ##{ffi_id}"
|
|
59
|
+
description: description || "Ruby callback ##{ffi_id}",
|
|
59
60
|
}
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -136,7 +137,7 @@ module Parsanol
|
|
|
136
137
|
ctx = DynamicContext.new(
|
|
137
138
|
context[:input],
|
|
138
139
|
context[:pos],
|
|
139
|
-
context[:captures].transform_keys(&:to_sym)
|
|
140
|
+
context[:captures].transform_keys(&:to_sym),
|
|
140
141
|
)
|
|
141
142
|
|
|
142
143
|
# Call the block
|
|
@@ -207,7 +208,7 @@ module Parsanol
|
|
|
207
208
|
# @return [String] The remaining input
|
|
208
209
|
#
|
|
209
210
|
def remaining
|
|
210
|
-
@input[@pos..] ||
|
|
211
|
+
@input[@pos..] || ""
|
|
211
212
|
end
|
|
212
213
|
|
|
213
214
|
# Check if at end of input
|
|
@@ -229,7 +230,7 @@ module Parsanol
|
|
|
229
230
|
if length
|
|
230
231
|
@input[@pos + start, length]
|
|
231
232
|
else
|
|
232
|
-
@input[@pos + start..]
|
|
233
|
+
@input[(@pos + start)..]
|
|
233
234
|
end
|
|
234
235
|
end
|
|
235
236
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "digest"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Native
|
|
@@ -17,11 +17,11 @@ module Parsanol
|
|
|
17
17
|
|
|
18
18
|
@cached_available = begin
|
|
19
19
|
# Try versioned path first (released gem), then non-versioned (local dev)
|
|
20
|
-
ruby_version = RUBY_VERSION.split(
|
|
20
|
+
ruby_version = RUBY_VERSION.split(".").take(2).join(".")
|
|
21
21
|
begin
|
|
22
22
|
require "parsanol/#{ruby_version}/parsanol_native"
|
|
23
23
|
rescue LoadError
|
|
24
|
-
require
|
|
24
|
+
require "parsanol/parsanol_native"
|
|
25
25
|
end
|
|
26
26
|
Parsanol::Native.is_available
|
|
27
27
|
rescue LoadError
|
|
@@ -54,7 +54,7 @@ module Parsanol
|
|
|
54
54
|
def cache_stats
|
|
55
55
|
{
|
|
56
56
|
hash_cache_size: GRAMMAR_HASH_CACHE.size,
|
|
57
|
-
grammar_cache_size: GRAMMAR_CACHE.size
|
|
57
|
+
grammar_cache_size: GRAMMAR_CACHE.size,
|
|
58
58
|
}
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -70,6 +70,7 @@ module Parsanol
|
|
|
70
70
|
if visited[obj_id]
|
|
71
71
|
return [:cycle, atom.class.name]
|
|
72
72
|
end
|
|
73
|
+
|
|
73
74
|
visited[obj_id] = true
|
|
74
75
|
|
|
75
76
|
case atom
|
|
@@ -89,7 +90,8 @@ module Parsanol
|
|
|
89
90
|
when ::Parsanol::Atoms::Named
|
|
90
91
|
[:named, atom.name.to_s, atom_structure(atom.parslet, visited)]
|
|
91
92
|
when ::Parsanol::Atoms::Lookahead
|
|
92
|
-
[:lookahead, atom.positive,
|
|
93
|
+
[:lookahead, atom.positive,
|
|
94
|
+
atom_structure(atom.bound_parslet, visited)]
|
|
93
95
|
else
|
|
94
96
|
[:unknown, atom.class.name]
|
|
95
97
|
end
|
|
@@ -78,9 +78,9 @@ module Parsanol
|
|
|
78
78
|
|
|
79
79
|
def serialize_str(atom)
|
|
80
80
|
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
81
|
+
"Str" => {
|
|
82
|
+
"pattern" => atom.str,
|
|
83
|
+
},
|
|
84
84
|
}
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -90,46 +90,46 @@ module Parsanol
|
|
|
90
90
|
pattern = atom.match
|
|
91
91
|
pattern = ::Regexp.last_match(1) if pattern =~ /^\(\?[-mix]*:(.+)\)$/
|
|
92
92
|
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
93
|
+
"Re" => {
|
|
94
|
+
"pattern" => pattern,
|
|
95
|
+
},
|
|
96
96
|
}
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def serialize_sequence(atom)
|
|
100
100
|
atom_ids = atom.parslets.map { |p| serialize_atom(p) }
|
|
101
101
|
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
102
|
+
"Sequence" => {
|
|
103
|
+
"atoms" => atom_ids,
|
|
104
|
+
},
|
|
105
105
|
}
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def serialize_alternative(atom)
|
|
109
109
|
atom_ids = atom.alternatives.map { |p| serialize_atom(p) }
|
|
110
110
|
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
111
|
+
"Alternative" => {
|
|
112
|
+
"atoms" => atom_ids,
|
|
113
|
+
},
|
|
114
114
|
}
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def serialize_repetition(atom)
|
|
118
118
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
119
|
+
"Repetition" => {
|
|
120
|
+
"atom" => serialize_atom(atom.parslet),
|
|
121
|
+
"min" => atom.min,
|
|
122
|
+
"max" => atom.max,
|
|
123
|
+
},
|
|
124
124
|
}
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def serialize_named(atom)
|
|
128
128
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
129
|
+
"Named" => {
|
|
130
|
+
"name" => atom.name.to_s,
|
|
131
|
+
"atom" => serialize_atom(atom.parslet),
|
|
132
|
+
},
|
|
133
133
|
}
|
|
134
134
|
end
|
|
135
135
|
|
|
@@ -169,7 +169,7 @@ module Parsanol
|
|
|
169
169
|
serialize_named(parslet)
|
|
170
170
|
when Parsanol::Atoms::Entity
|
|
171
171
|
# Nested entity - just reference it via serialize_atom
|
|
172
|
-
{
|
|
172
|
+
{ "Entity" => { "atom" => serialize_atom(parslet) } }
|
|
173
173
|
when Parsanol::Atoms::Lookahead
|
|
174
174
|
serialize_lookahead(parslet)
|
|
175
175
|
else
|
|
@@ -181,9 +181,9 @@ module Parsanol
|
|
|
181
181
|
else
|
|
182
182
|
# If the entity's block returns nil, create a placeholder that will fail
|
|
183
183
|
@atoms[atom_id] = {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
184
|
+
"Str" => {
|
|
185
|
+
"pattern" => "\x00__UNIMPLEMENTED_ENTITY_#{atom.name}__",
|
|
186
|
+
},
|
|
187
187
|
}
|
|
188
188
|
end
|
|
189
189
|
atom_id
|
|
@@ -191,10 +191,10 @@ module Parsanol
|
|
|
191
191
|
|
|
192
192
|
def serialize_lookahead(atom)
|
|
193
193
|
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
194
|
+
"Lookahead" => {
|
|
195
|
+
"atom" => serialize_atom(atom.bound_parslet),
|
|
196
|
+
"positive" => atom.positive,
|
|
197
|
+
},
|
|
198
198
|
}
|
|
199
199
|
end
|
|
200
200
|
|
|
@@ -202,10 +202,10 @@ module Parsanol
|
|
|
202
202
|
# Capture stores matched text for later reference by Dynamic atoms.
|
|
203
203
|
# Now properly serialized for native parser support (parsanol-rs 0.3.0+).
|
|
204
204
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
205
|
+
"Capture" => {
|
|
206
|
+
"name" => atom.capture_key.to_s,
|
|
207
|
+
"atom" => serialize_atom(atom.inner_atom),
|
|
208
|
+
},
|
|
209
209
|
}
|
|
210
210
|
end
|
|
211
211
|
|
|
@@ -220,9 +220,9 @@ module Parsanol
|
|
|
220
220
|
return serialize_unknown(atom) unless inner
|
|
221
221
|
|
|
222
222
|
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
223
|
+
"Scope" => {
|
|
224
|
+
"atom" => serialize_atom(inner),
|
|
225
|
+
},
|
|
226
226
|
}
|
|
227
227
|
end
|
|
228
228
|
|
|
@@ -232,9 +232,9 @@ module Parsanol
|
|
|
232
232
|
callback_id = Parsanol::Native::Dynamic.register(atom.block)
|
|
233
233
|
|
|
234
234
|
{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
235
|
+
"Dynamic" => {
|
|
236
|
+
"callback_id" => callback_id,
|
|
237
|
+
},
|
|
238
238
|
}
|
|
239
239
|
end
|
|
240
240
|
|
|
@@ -242,9 +242,9 @@ module Parsanol
|
|
|
242
242
|
# For unsupported atom types, create a placeholder
|
|
243
243
|
# This will cause a parse error at runtime
|
|
244
244
|
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
245
|
+
"Str" => {
|
|
246
|
+
"pattern" => "", # Empty pattern that will never match
|
|
247
|
+
},
|
|
248
248
|
}
|
|
249
249
|
end
|
|
250
250
|
end
|
|
@@ -18,9 +18,9 @@ module Parsanol
|
|
|
18
18
|
#
|
|
19
19
|
class AstTransformer
|
|
20
20
|
# Frozen string constants for tag comparisons (avoid allocations)
|
|
21
|
-
SEQUENCE_TAG =
|
|
22
|
-
REPETITION_TAG =
|
|
23
|
-
EMPTY_STRING =
|
|
21
|
+
SEQUENCE_TAG = ":sequence"
|
|
22
|
+
REPETITION_TAG = ":repetition"
|
|
23
|
+
EMPTY_STRING = ""
|
|
24
24
|
EMPTY_ARRAY = [].freeze
|
|
25
25
|
EMPTY_HASH = {}.freeze
|
|
26
26
|
|
|
@@ -62,7 +62,7 @@ module Parsanol
|
|
|
62
62
|
# Check if this is a tagged array from native parser
|
|
63
63
|
# Native parser produces Symbol tags: [:sequence, item1, item2, ...]
|
|
64
64
|
first = arr.first
|
|
65
|
-
if
|
|
65
|
+
if [SEQUENCE_SYM, SEQUENCE_TAG].include?(first)
|
|
66
66
|
# Optimized: transform items starting from index 1
|
|
67
67
|
# Avoid creating arr[1..] slice
|
|
68
68
|
len = arr.length
|
|
@@ -75,7 +75,7 @@ module Parsanol
|
|
|
75
75
|
i += 1
|
|
76
76
|
end
|
|
77
77
|
flatten_sequence(items)
|
|
78
|
-
elsif
|
|
78
|
+
elsif [REPETITION_SYM, REPETITION_TAG].include?(first)
|
|
79
79
|
# Optimized: transform items starting from index 1
|
|
80
80
|
len = arr.length
|
|
81
81
|
return EMPTY_ARRAY if len == 1
|
|
@@ -87,7 +87,7 @@ module Parsanol
|
|
|
87
87
|
i += 1
|
|
88
88
|
end
|
|
89
89
|
flatten_repetition(items)
|
|
90
|
-
elsif first.is_a?(Symbol) || (first.is_a?(String) && first.start_with?(
|
|
90
|
+
elsif first.is_a?(Symbol) || (first.is_a?(String) && first.start_with?(":"))
|
|
91
91
|
# Other tagged arrays - pass through
|
|
92
92
|
arr.map { |item| transform(item) }
|
|
93
93
|
else
|
|
@@ -119,13 +119,15 @@ module Parsanol
|
|
|
119
119
|
|
|
120
120
|
# Check if value is a tagged repetition from native parser
|
|
121
121
|
is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
|
|
122
|
-
|
|
122
|
+
value.first.is_a?(String) && value.first == REPETITION_TAG
|
|
123
123
|
|
|
124
124
|
# Check RAW value for repetition pattern BEFORE transformation
|
|
125
125
|
# Array with items that all have the parent key
|
|
126
126
|
# e.g., [{x: 1}, {x: 2}] where parent key is :x
|
|
127
127
|
is_raw_array_repetition = value.is_a?(Array) && !value.empty? &&
|
|
128
|
-
|
|
128
|
+
value.all? do |item|
|
|
129
|
+
item.is_a?(Hash) && item.keys.length == 1 && item.key?(key)
|
|
130
|
+
end
|
|
129
131
|
|
|
130
132
|
# Empty array from native parser is a repetition result (not a sequence)
|
|
131
133
|
# Sequences produce arrays of arrays like [[], []], not empty arrays
|
|
@@ -134,12 +136,15 @@ module Parsanol
|
|
|
134
136
|
# Special handling for arrays that look like character repetitions
|
|
135
137
|
# (arrays of single-character Slices/strings should be joined)
|
|
136
138
|
if transformed.is_a?(Array) && !transformed.empty? &&
|
|
137
|
-
|
|
139
|
+
transformed.all? do |item|
|
|
140
|
+
slice_or_string?(item) && item_length(item) == 1
|
|
141
|
+
end
|
|
138
142
|
# Join preserving position from first Slice
|
|
139
143
|
first_slice = transformed.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
140
144
|
content = transformed.map { |i| slice_content(i) }.join
|
|
141
145
|
transformed = if first_slice
|
|
142
|
-
::Parsanol::Slice.new(first_slice.offset, content,
|
|
146
|
+
::Parsanol::Slice.new(first_slice.offset, content,
|
|
147
|
+
first_slice.input)
|
|
143
148
|
else
|
|
144
149
|
content
|
|
145
150
|
end
|
|
@@ -148,9 +153,9 @@ module Parsanol
|
|
|
148
153
|
# Check for UNTAGGED repetition pattern (native output):
|
|
149
154
|
# If array items all have the same key as parent, it's a repetition
|
|
150
155
|
is_transformed_repetition = transformed.is_a?(Array) && !transformed.empty? &&
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
transformed.all? do |item|
|
|
157
|
+
item.is_a?(Hash) && item.keys.length == 1 && item.key?(sym_key)
|
|
158
|
+
end
|
|
154
159
|
|
|
155
160
|
is_repetition = is_tagged_repetition || is_raw_array_repetition || is_transformed_repetition || is_empty_repetition
|
|
156
161
|
|
|
@@ -184,7 +189,9 @@ module Parsanol
|
|
|
184
189
|
if transformed.empty?
|
|
185
190
|
{ sym_key => EMPTY_ARRAY }
|
|
186
191
|
# Check if items already have the same key (avoid double-wrapping)
|
|
187
|
-
elsif transformed.all?
|
|
192
|
+
elsif transformed.all? do |item|
|
|
193
|
+
item.is_a?(Hash) && item.key?(sym_key)
|
|
194
|
+
end
|
|
188
195
|
{ sym_key => transformed }
|
|
189
196
|
else
|
|
190
197
|
# Wrap each item with the name
|
|
@@ -207,7 +214,9 @@ module Parsanol
|
|
|
207
214
|
# We can't tell from the value alone, so we return empty Slice (sequence semantics)
|
|
208
215
|
# The repetition detection in transform_single_key_hash will handle the other case
|
|
209
216
|
{ sym_key => ::Parsanol::Slice.new(0, EMPTY_STRING, nil) }
|
|
210
|
-
elsif transformed.all?
|
|
217
|
+
elsif transformed.all? do |v|
|
|
218
|
+
v.is_a?(Hash) && v.keys.length == 1 && v.key?(sym_key)
|
|
219
|
+
end
|
|
211
220
|
# Items already have the parent key (repetition pattern) - keep as-is
|
|
212
221
|
{ sym_key => transformed }
|
|
213
222
|
elsif transformed.all?(Hash)
|
|
@@ -229,13 +238,15 @@ module Parsanol
|
|
|
229
238
|
sym_key = cached_symbol(key)
|
|
230
239
|
|
|
231
240
|
is_repetition = value.is_a?(Array) && !value.empty? &&
|
|
232
|
-
|
|
241
|
+
value.first.is_a?(String) && value.first == REPETITION_TAG
|
|
233
242
|
|
|
234
243
|
transformed = transform(value)
|
|
235
244
|
|
|
236
245
|
result[sym_key] = if is_repetition
|
|
237
246
|
if transformed.is_a?(Array)
|
|
238
|
-
if transformed.all?
|
|
247
|
+
if transformed.all? do |item|
|
|
248
|
+
item.is_a?(Hash) && item.key?(sym_key)
|
|
249
|
+
end
|
|
239
250
|
transformed
|
|
240
251
|
else
|
|
241
252
|
transformed.map { |item| { sym_key => item } }
|
|
@@ -308,7 +319,7 @@ module Parsanol
|
|
|
308
319
|
else
|
|
309
320
|
# Check if array contains only hashes (repetition wrapper pattern)
|
|
310
321
|
# In this case, merge the inner hashes into merged_hash
|
|
311
|
-
non_hash_items = item.
|
|
322
|
+
non_hash_items = item.grep_v(Hash)
|
|
312
323
|
all_items_are_hashes = non_hash_items.empty?
|
|
313
324
|
|
|
314
325
|
if all_items_are_hashes
|
|
@@ -399,12 +410,16 @@ module Parsanol
|
|
|
399
410
|
# REPETITION pattern (same keys like entity_decl): keep as array
|
|
400
411
|
# WRAPPER pattern (different keys like spaces vs schemaDecl): merge
|
|
401
412
|
first_inner_keys = items.first[wrapper_key].keys.to_set
|
|
402
|
-
|
|
413
|
+
items.all? do |item|
|
|
414
|
+
item[wrapper_key].keys.to_set == first_inner_keys
|
|
415
|
+
end
|
|
403
416
|
|
|
404
417
|
# Check if items have single keys or multiple keys
|
|
405
418
|
# - Single key items with repeated outer key = true repetition (keep array)
|
|
406
419
|
# - Multiple key items with repeated outer key = duplicate labels in sequence (merge)
|
|
407
|
-
max_keys_per_item = items.map
|
|
420
|
+
max_keys_per_item = items.map do |item|
|
|
421
|
+
item.is_a?(Hash) ? item.keys.length : 0
|
|
422
|
+
end.max || 0
|
|
408
423
|
|
|
409
424
|
# Check if inner values are hashes with different keys
|
|
410
425
|
# This distinguishes:
|
|
@@ -412,9 +427,13 @@ module Parsanol
|
|
|
412
427
|
# - Duplicate labels: [{group: {char: 'a'}}, {group: {digit: '5'}}] - inner is hash with different keys
|
|
413
428
|
inner_keys_all_same = true
|
|
414
429
|
first_inner_keys = nil
|
|
415
|
-
if items.all?
|
|
430
|
+
if items.all? do |item|
|
|
431
|
+
item.is_a?(Hash) && item[wrapper_key].is_a?(Hash)
|
|
432
|
+
end
|
|
416
433
|
first_inner_keys = items.first[wrapper_key].keys.to_set
|
|
417
|
-
inner_keys_all_same = items.all?
|
|
434
|
+
inner_keys_all_same = items.all? do |item|
|
|
435
|
+
item[wrapper_key].keys.to_set == first_inner_keys
|
|
436
|
+
end
|
|
418
437
|
end
|
|
419
438
|
|
|
420
439
|
# DUPLICATE LABELS IN SEQUENCE: multiple keys per item with repeated outer key
|
|
@@ -426,7 +445,9 @@ module Parsanol
|
|
|
426
445
|
|
|
427
446
|
# Check if inner hashes have the same keys or different keys
|
|
428
447
|
first_inner_keys ||= items.first[wrapper_key].keys.to_set
|
|
429
|
-
all_same_keys = items.all?
|
|
448
|
+
all_same_keys = items.all? do |item|
|
|
449
|
+
item[wrapper_key].keys.to_set == first_inner_keys
|
|
450
|
+
end
|
|
430
451
|
|
|
431
452
|
if has_duplicate_labels
|
|
432
453
|
# DUPLICATE LABELS PATTERN: items have multiple keys with repeated outer key
|
|
@@ -436,7 +457,7 @@ module Parsanol
|
|
|
436
457
|
merged = {}
|
|
437
458
|
items.each do |item|
|
|
438
459
|
item.each do |k, v|
|
|
439
|
-
merged[k] = v
|
|
460
|
+
merged[k] = v # Last value wins
|
|
440
461
|
end
|
|
441
462
|
end
|
|
442
463
|
# Return only the wrapper key with its last value
|
|
@@ -475,11 +496,14 @@ module Parsanol
|
|
|
475
496
|
if slice_or_string_parts.any?
|
|
476
497
|
# Join Slices/strings, preserving position from first Slice
|
|
477
498
|
first_slice = slice_or_string_parts.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
478
|
-
content = slice_or_string_parts.map
|
|
499
|
+
content = slice_or_string_parts.map do |i|
|
|
500
|
+
i.is_a?(::Parsanol::Slice) ? i.content : i
|
|
501
|
+
end.join
|
|
479
502
|
|
|
480
503
|
if first_slice
|
|
481
504
|
# Create new Slice with combined content, preserving position from first
|
|
482
|
-
return ::Parsanol::Slice.new(first_slice.offset, content,
|
|
505
|
+
return ::Parsanol::Slice.new(first_slice.offset, content,
|
|
506
|
+
first_slice.input)
|
|
483
507
|
else
|
|
484
508
|
# All plain strings (shouldn't happen with new decode_flat, but handle it)
|
|
485
509
|
return slice_or_string_parts.length == 1 ? slice_or_string_parts.first : content
|
|
@@ -520,11 +544,14 @@ module Parsanol
|
|
|
520
544
|
# If all Slices or strings, join them preserving position from first Slice
|
|
521
545
|
if all_slices_or_strings
|
|
522
546
|
first_slice = flat_items.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
523
|
-
content = flat_items.map
|
|
547
|
+
content = flat_items.map do |i|
|
|
548
|
+
i.is_a?(::Parsanol::Slice) ? i.content : i
|
|
549
|
+
end.join
|
|
524
550
|
|
|
525
551
|
if first_slice
|
|
526
552
|
# Create new Slice with combined content, preserving position from first
|
|
527
|
-
::Parsanol::Slice.new(first_slice.offset, content,
|
|
553
|
+
::Parsanol::Slice.new(first_slice.offset, content,
|
|
554
|
+
first_slice.input)
|
|
528
555
|
else
|
|
529
556
|
# All plain strings (shouldn't happen with new decode_flat, but handle it)
|
|
530
557
|
content
|
|
@@ -19,9 +19,9 @@ module Parsanol
|
|
|
19
19
|
TAG_INLINE_STRING = 0x0A
|
|
20
20
|
|
|
21
21
|
# Frozen string constants for transformer (avoid allocations)
|
|
22
|
-
SEQUENCE_TAG =
|
|
23
|
-
REPETITION_TAG =
|
|
24
|
-
EMPTY_STRING =
|
|
22
|
+
SEQUENCE_TAG = ":sequence"
|
|
23
|
+
REPETITION_TAG = ":repetition"
|
|
24
|
+
EMPTY_STRING = ""
|
|
25
25
|
EMPTY_ARRAY = [].freeze
|
|
26
26
|
EMPTY_HASH = {}.freeze
|
|
27
27
|
end
|