parsanol 1.0.1-aarch64-linux → 1.0.2-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/README.adoc +262 -35
- data/Rakefile +9 -3
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/native/parser.rb +46 -18
- data/lib/parsanol/native/transformer.rb +74 -31
- data/lib/parsanol/options/zero_copy.rb +1 -1
- data/lib/parsanol/options.rb +8 -7
- data/lib/parsanol/parser.rb +24 -7
- data/lib/parsanol/slice.rb +61 -0
- data/lib/parsanol/version.rb +1 -1
- data/{parsanol-ruby.gemspec → parsanol.gemspec} +1 -1
- metadata +3 -6
- data/lib/parsanol/lexer.rb +0 -144
- data/lib/parsanol/options/ruby_transform.rb +0 -107
- data/lib/parsanol/options/serialized.rb +0 -94
|
@@ -5,15 +5,15 @@ module Parsanol
|
|
|
5
5
|
# Transforms native AST format to Parslet-compatible format
|
|
6
6
|
#
|
|
7
7
|
# Native format from Rust parser:
|
|
8
|
-
# -
|
|
8
|
+
# - Slices: Parsanol::Slice objects (with position info)
|
|
9
9
|
# - Sequences: [":sequence", item1, item2, ...]
|
|
10
10
|
# - Repetitions: [":repetition", item1, item2, ...]
|
|
11
11
|
# - Named captures: {"name" => value}
|
|
12
12
|
#
|
|
13
13
|
# Parslet format:
|
|
14
|
-
# -
|
|
14
|
+
# - Slices: Preserved as Parsanol::Slice (with position info)
|
|
15
15
|
# - Sequences: merged hash {:key1 => val1, :key2 => val2, ...}
|
|
16
|
-
# - Repetitions: array of items (or
|
|
16
|
+
# - Repetitions: array of items (or joined Slices if all are Slices)
|
|
17
17
|
# - Named wrapping Repetition: {:name => [{:name => item1}, {:name => item2}, ...]}
|
|
18
18
|
#
|
|
19
19
|
class AstTransformer
|
|
@@ -128,10 +128,17 @@ module Parsanol
|
|
|
128
128
|
transformed = transform(value)
|
|
129
129
|
|
|
130
130
|
# Special handling for arrays that look like character repetitions
|
|
131
|
-
# (arrays of single-character strings should be joined)
|
|
131
|
+
# (arrays of single-character Slices/strings should be joined)
|
|
132
132
|
if transformed.is_a?(Array) && !transformed.empty? &&
|
|
133
|
-
transformed.all? { |item|
|
|
134
|
-
|
|
133
|
+
transformed.all? { |item| slice_or_string?(item) && item_length(item) == 1 }
|
|
134
|
+
# Join preserving position from first Slice
|
|
135
|
+
first_slice = transformed.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
136
|
+
content = transformed.map { |i| slice_content(i) }.join
|
|
137
|
+
transformed = if first_slice
|
|
138
|
+
::Parsanol::Slice.new(first_slice.offset, content, first_slice.position_cache)
|
|
139
|
+
else
|
|
140
|
+
content
|
|
141
|
+
end
|
|
135
142
|
end
|
|
136
143
|
|
|
137
144
|
# Check for UNTAGGED repetition pattern (native output):
|
|
@@ -151,11 +158,21 @@ module Parsanol
|
|
|
151
158
|
elsif transformed.is_a?(Array)
|
|
152
159
|
transform_array_value(sym_key, transformed)
|
|
153
160
|
else
|
|
154
|
-
# Simple value (string, nil, etc.) - most common case
|
|
161
|
+
# Simple value (Slice, string, nil, etc.) - most common case
|
|
155
162
|
{ sym_key => transformed }
|
|
156
163
|
end
|
|
157
164
|
end
|
|
158
165
|
|
|
166
|
+
# Get content from Slice or string
|
|
167
|
+
def self.slice_content(value)
|
|
168
|
+
value.is_a?(::Parsanol::Slice) ? value.content : value.to_s
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get length of Slice or string
|
|
172
|
+
def self.item_length(value)
|
|
173
|
+
value.is_a?(::Parsanol::Slice) ? value.length : value.to_s.length
|
|
174
|
+
end
|
|
175
|
+
|
|
159
176
|
# Handle repetition values (named wrapping repetition)
|
|
160
177
|
def self.transform_repetition_value(sym_key, transformed)
|
|
161
178
|
if transformed.is_a?(Array)
|
|
@@ -169,7 +186,9 @@ module Parsanol
|
|
|
169
186
|
# Wrap each item with the name
|
|
170
187
|
{ sym_key => transformed.map { |item| { sym_key => item } } }
|
|
171
188
|
end
|
|
172
|
-
elsif transformed
|
|
189
|
+
elsif transformed.is_a?(::Parsanol::Slice) && transformed.empty?
|
|
190
|
+
{ sym_key => EMPTY_ARRAY } # Empty repetition should be [], not empty Slice
|
|
191
|
+
elsif transformed.is_a?(String) && transformed == EMPTY_STRING
|
|
173
192
|
{ sym_key => EMPTY_ARRAY } # Empty repetition should be [], not ""
|
|
174
193
|
else
|
|
175
194
|
{ sym_key => transformed }
|
|
@@ -180,10 +199,10 @@ module Parsanol
|
|
|
180
199
|
def self.transform_array_value(sym_key, transformed)
|
|
181
200
|
if transformed.empty?
|
|
182
201
|
# For empty arrays, we need to determine if this is a repetition or sequence
|
|
183
|
-
# Repetitions should return [], sequences should return
|
|
184
|
-
# We can't tell from the value alone, so we return
|
|
202
|
+
# Repetitions should return [], sequences should return empty Slice
|
|
203
|
+
# We can't tell from the value alone, so we return empty Slice (sequence semantics)
|
|
185
204
|
# The repetition detection in transform_single_key_hash will handle the other case
|
|
186
|
-
{ sym_key => EMPTY_STRING }
|
|
205
|
+
{ sym_key => ::Parsanol::Slice.new(0, EMPTY_STRING, nil) }
|
|
187
206
|
elsif transformed.all? { |v| v.is_a?(Hash) && v.keys.length == 1 && v.key?(sym_key) }
|
|
188
207
|
# Items already have the parent key (repetition pattern) - keep as-is
|
|
189
208
|
{ sym_key => transformed }
|
|
@@ -242,8 +261,8 @@ module Parsanol
|
|
|
242
261
|
|
|
243
262
|
# Flatten sequence items according to Parslet semantics:
|
|
244
263
|
# 1. If ALL items are hashes, return as array (this is a repetition result)
|
|
245
|
-
# 2. If there are named captures (hashes) among strings, return ONLY the merged hash (discard strings)
|
|
246
|
-
# 3. If only strings, join them
|
|
264
|
+
# 2. If there are named captures (hashes) among Slices/strings, return ONLY the merged hash (discard Slices/strings)
|
|
265
|
+
# 3. If only Slices/strings, join them preserving position from first Slice
|
|
247
266
|
# 4. Return single value if only one item
|
|
248
267
|
#
|
|
249
268
|
# This matches Parslet's behavior where:
|
|
@@ -264,7 +283,7 @@ module Parsanol
|
|
|
264
283
|
|
|
265
284
|
# Single pass: categorize items
|
|
266
285
|
merged_hash = {}
|
|
267
|
-
|
|
286
|
+
slice_or_string_parts = []
|
|
268
287
|
hash_count = 0
|
|
269
288
|
total_items = 0
|
|
270
289
|
has_non_empty_array = false
|
|
@@ -275,8 +294,8 @@ module Parsanol
|
|
|
275
294
|
merged_hash.merge!(item)
|
|
276
295
|
hash_count += 1
|
|
277
296
|
total_items += 1
|
|
278
|
-
when String
|
|
279
|
-
|
|
297
|
+
when ::Parsanol::Slice, String
|
|
298
|
+
slice_or_string_parts << item
|
|
280
299
|
total_items += 1
|
|
281
300
|
when Array
|
|
282
301
|
# Check if this is a non-empty array (repetition result with content)
|
|
@@ -292,8 +311,8 @@ module Parsanol
|
|
|
292
311
|
case sub_item
|
|
293
312
|
when Hash
|
|
294
313
|
hash_count += 1
|
|
295
|
-
when String
|
|
296
|
-
|
|
314
|
+
when ::Parsanol::Slice, String
|
|
315
|
+
slice_or_string_parts << sub_item
|
|
297
316
|
end
|
|
298
317
|
end
|
|
299
318
|
end
|
|
@@ -320,8 +339,8 @@ module Parsanol
|
|
|
320
339
|
result << item
|
|
321
340
|
when Array
|
|
322
341
|
result.concat(item)
|
|
323
|
-
when String
|
|
324
|
-
# Skip unnamed strings when we have named captures
|
|
342
|
+
when ::Parsanol::Slice, String
|
|
343
|
+
# Skip unnamed Slices/strings when we have named captures
|
|
325
344
|
end
|
|
326
345
|
end
|
|
327
346
|
return result.length == 1 ? result.first : result
|
|
@@ -382,12 +401,22 @@ module Parsanol
|
|
|
382
401
|
|
|
383
402
|
# PARSLET SEQUENCE SEMANTICS:
|
|
384
403
|
# If there are named captures (hashes) mixed with other things,
|
|
385
|
-
# return ONLY the merged hash (discard unnamed strings)
|
|
404
|
+
# return ONLY the merged hash (discard unnamed Slices/strings)
|
|
386
405
|
return merged_hash unless merged_hash.empty?
|
|
387
406
|
|
|
388
|
-
# No named captures - handle strings and other items
|
|
389
|
-
if
|
|
390
|
-
|
|
407
|
+
# No named captures - handle Slices/strings and other items
|
|
408
|
+
if slice_or_string_parts.any?
|
|
409
|
+
# Join Slices/strings, preserving position from first Slice
|
|
410
|
+
first_slice = slice_or_string_parts.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
411
|
+
content = slice_or_string_parts.map { |i| i.is_a?(::Parsanol::Slice) ? i.content : i }.join
|
|
412
|
+
|
|
413
|
+
if first_slice
|
|
414
|
+
# Create new Slice with combined content, preserving position from first
|
|
415
|
+
::Parsanol::Slice.new(first_slice.offset, content, first_slice.position_cache)
|
|
416
|
+
else
|
|
417
|
+
# All plain strings (shouldn't happen with new decode_flat, but handle it)
|
|
418
|
+
slice_or_string_parts.length == 1 ? slice_or_string_parts.first : content
|
|
419
|
+
end
|
|
391
420
|
end
|
|
392
421
|
|
|
393
422
|
# Only other items (arrays, etc.)
|
|
@@ -398,36 +427,50 @@ module Parsanol
|
|
|
398
427
|
|
|
399
428
|
# Parslet/Parsanol repetition semantics:
|
|
400
429
|
# 1. Return [] for empty repetitions
|
|
401
|
-
# 2. If all items are strings, join them
|
|
430
|
+
# 2. If all items are Slices (or strings), join them preserving position
|
|
402
431
|
# 3. Otherwise return array
|
|
403
432
|
def self.flatten_repetition(items)
|
|
404
433
|
return EMPTY_ARRAY if items.empty?
|
|
405
434
|
|
|
406
435
|
# Single-pass flatten and check
|
|
407
436
|
flat_items = []
|
|
408
|
-
|
|
437
|
+
all_slices_or_strings = true
|
|
409
438
|
|
|
410
439
|
items.each do |item|
|
|
411
440
|
if item.is_a?(Array)
|
|
412
441
|
item.each do |sub|
|
|
413
442
|
flat_items << sub
|
|
414
|
-
|
|
443
|
+
all_slices_or_strings = false unless slice_or_string?(sub)
|
|
415
444
|
end
|
|
416
445
|
else
|
|
417
446
|
flat_items << item
|
|
418
|
-
|
|
447
|
+
all_slices_or_strings = false unless slice_or_string?(item)
|
|
419
448
|
end
|
|
420
449
|
end
|
|
421
450
|
|
|
422
451
|
return EMPTY_ARRAY if flat_items.empty?
|
|
423
452
|
|
|
424
|
-
# If all strings, join them
|
|
425
|
-
if
|
|
426
|
-
flat_items.
|
|
453
|
+
# If all Slices or strings, join them preserving position from first Slice
|
|
454
|
+
if all_slices_or_strings
|
|
455
|
+
first_slice = flat_items.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
456
|
+
content = flat_items.map { |i| i.is_a?(::Parsanol::Slice) ? i.content : i }.join
|
|
457
|
+
|
|
458
|
+
if first_slice
|
|
459
|
+
# Create new Slice with combined content, preserving position from first
|
|
460
|
+
::Parsanol::Slice.new(first_slice.offset, content, first_slice.position_cache)
|
|
461
|
+
else
|
|
462
|
+
# All plain strings (shouldn't happen with new decode_flat, but handle it)
|
|
463
|
+
content
|
|
464
|
+
end
|
|
427
465
|
else
|
|
428
466
|
flat_items
|
|
429
467
|
end
|
|
430
468
|
end
|
|
469
|
+
|
|
470
|
+
# Check if value is a Slice or String
|
|
471
|
+
def self.slice_or_string?(value)
|
|
472
|
+
value.is_a?(::Parsanol::Slice) || value.is_a?(String)
|
|
473
|
+
end
|
|
431
474
|
end
|
|
432
475
|
|
|
433
476
|
private_constant :AstTransformer
|
|
@@ -98,7 +98,7 @@ module Parsanol
|
|
|
98
98
|
raise LoadError,
|
|
99
99
|
"ZeroCopy mode requires native extension for direct FFI object construction. " \
|
|
100
100
|
"Run `rake compile` to build the extension, or use " \
|
|
101
|
-
"
|
|
101
|
+
"parser.parse(input, mode: :ruby) for pure Ruby parsing."
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
grammar_json = Parsanol::Native.serialize_grammar(root)
|
data/lib/parsanol/options.rb
CHANGED
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# Parsanol Transform Mode Options
|
|
4
4
|
#
|
|
5
|
-
# This module provides
|
|
5
|
+
# This module provides the ZeroCopy transformation mode for maximum performance:
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# 2. Serialized - Parse + Transform in Rust, JSON output (requires native extension)
|
|
9
|
-
# 3. ZeroCopy - Direct FFI object construction (requires native extension, fastest)
|
|
7
|
+
# ZeroCopy - Direct FFI object construction (requires native extension, fastest)
|
|
10
8
|
#
|
|
11
9
|
# Usage:
|
|
12
10
|
# class MyParser < Parsanol::Parser
|
|
13
|
-
# include Parsanol::
|
|
11
|
+
# include Parsanol::ZeroCopy
|
|
14
12
|
# rule(:number) { match('[0-9]').repeat(1).as(:int) }
|
|
15
13
|
# root(:number)
|
|
14
|
+
#
|
|
15
|
+
# output_types(number: MyNumberClass)
|
|
16
16
|
# end
|
|
17
|
+
#
|
|
18
|
+
# For standard parsing, use the Parse Modes API instead:
|
|
19
|
+
# parser.parse(input, mode: :native) # or :ruby, :json
|
|
17
20
|
|
|
18
|
-
require 'parsanol/options/ruby_transform'
|
|
19
|
-
require 'parsanol/options/serialized'
|
|
20
21
|
require 'parsanol/options/zero_copy'
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -82,22 +82,32 @@ module Parsanol
|
|
|
82
82
|
|
|
83
83
|
# Unified parsing interface with mode selection support.
|
|
84
84
|
#
|
|
85
|
+
# All parse modes return results with Slice objects that contain
|
|
86
|
+
# position information (offset, length, line, column). This enables
|
|
87
|
+
# source code extraction, error reporting, and remark attachment.
|
|
88
|
+
#
|
|
85
89
|
# @param input [String] the string to parse
|
|
86
90
|
# @param mode_or_opts [Symbol, Hash] parsing mode or options hash
|
|
87
91
|
# @param kwargs [Hash] additional keyword options
|
|
88
92
|
#
|
|
89
93
|
# Modes:
|
|
90
|
-
# - :ruby - Pure Ruby parsing (
|
|
94
|
+
# - :ruby - Pure Ruby parsing (always available, returns Slices with position)
|
|
91
95
|
# - :native - Use Rust extension if available, fallback to Ruby
|
|
92
|
-
# - :json - Return JSON string
|
|
96
|
+
# - :json - Return JSON string with position info for each value
|
|
93
97
|
#
|
|
94
98
|
# Options:
|
|
95
99
|
# - :reporter - Custom error reporter instance
|
|
96
100
|
# - :prefix - Allow partial matching (default: false)
|
|
97
101
|
#
|
|
98
|
-
# @return [Hash, Array,
|
|
102
|
+
# @return [Hash, Array, Parsanol::Slice] parsed result with position info
|
|
99
103
|
# @raise [Parsanol::ParseFailed] when parsing fails
|
|
100
104
|
#
|
|
105
|
+
# @example Parse and access position info
|
|
106
|
+
# result = parser.parse("hello")
|
|
107
|
+
# result[:name].offset # => 0
|
|
108
|
+
# result[:name].line_and_column # => [1, 1]
|
|
109
|
+
# result[:name].to_s # => "hello"
|
|
110
|
+
#
|
|
101
111
|
def parse(input, mode_or_opts = {}, **kwargs)
|
|
102
112
|
if mode_or_opts.is_a?(Hash)
|
|
103
113
|
# Legacy API: parse(input, options={})
|
|
@@ -150,24 +160,31 @@ module Parsanol
|
|
|
150
160
|
#
|
|
151
161
|
|
|
152
162
|
# Native extension parsing with Ruby fallback.
|
|
163
|
+
# Returns results with Slice objects containing position info.
|
|
153
164
|
#
|
|
154
165
|
# @param input [String] input to parse
|
|
155
166
|
# @param opts [Hash] parsing options
|
|
156
|
-
# @return [Object] parse result
|
|
167
|
+
# @return [Object] parse result with Slice objects for position info
|
|
157
168
|
#
|
|
158
169
|
def parse_native(input, opts)
|
|
159
170
|
if Parsanol::Native.available?
|
|
160
|
-
|
|
171
|
+
# Build line cache for position info
|
|
172
|
+
line_cache = Parsanol::Source::LineCache.new
|
|
173
|
+
line_cache.scan_for_line_endings(0, input)
|
|
174
|
+
|
|
175
|
+
# Parse with position info (now the default)
|
|
176
|
+
Parsanol::Native.parse_parslet_compatible(root, input, line_cache)
|
|
161
177
|
else
|
|
162
178
|
parse_ruby(input, opts)
|
|
163
179
|
end
|
|
164
180
|
end
|
|
165
181
|
|
|
166
|
-
# JSON output mode - returns JSON
|
|
182
|
+
# JSON output mode - returns JSON with position info.
|
|
183
|
+
# All Slice values are serialized with their position information.
|
|
167
184
|
#
|
|
168
185
|
# @param input [String] input to parse
|
|
169
186
|
# @param opts [Hash] parsing options
|
|
170
|
-
# @return [String] JSON representation
|
|
187
|
+
# @return [String] JSON representation with position info
|
|
171
188
|
#
|
|
172
189
|
def parse_json(input, opts)
|
|
173
190
|
if Parsanol::Native.available?
|
data/lib/parsanol/slice.rb
CHANGED
|
@@ -142,5 +142,66 @@ module Parsanol
|
|
|
142
142
|
def inspect
|
|
143
143
|
"#{content.inspect}@#{offset}"
|
|
144
144
|
end
|
|
145
|
+
|
|
146
|
+
# JSON serialization --------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
# JSON serialization returns the full object with position info.
|
|
149
|
+
# This is the default behavior - position info is ALWAYS included.
|
|
150
|
+
#
|
|
151
|
+
# @return [String] JSON representation with value and position
|
|
152
|
+
def to_json(*)
|
|
153
|
+
as_json.to_json(*)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns a hash with full position information for JSON serialization.
|
|
157
|
+
# Line and column are always included when a position cache is available.
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash] hash with value, offset, length, and line/column
|
|
160
|
+
def as_json(_options = {})
|
|
161
|
+
result = {
|
|
162
|
+
'value' => content,
|
|
163
|
+
'offset' => offset,
|
|
164
|
+
'length' => length
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if position_cache
|
|
168
|
+
line, column = line_and_column
|
|
169
|
+
result['line'] = line
|
|
170
|
+
result['column'] = column
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
result
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Returns a SourceSpan representing this slice's position
|
|
177
|
+
#
|
|
178
|
+
# @param input [String, nil] the original input (needed for line/column)
|
|
179
|
+
# @return [Parsanol::SourceSpan] span object
|
|
180
|
+
def to_span(_input = nil)
|
|
181
|
+
start_pos = if position_cache
|
|
182
|
+
line, column = line_and_column
|
|
183
|
+
SourcePosition.new(offset: offset, line: line, column: column)
|
|
184
|
+
else
|
|
185
|
+
SourcePosition.new(offset: offset, line: 1, column: 1)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
end_offset = offset + length
|
|
189
|
+
end_pos = if position_cache
|
|
190
|
+
line, column = position_cache.line_and_column(end_offset)
|
|
191
|
+
SourcePosition.new(offset: end_offset, line: line, column: column)
|
|
192
|
+
else
|
|
193
|
+
SourcePosition.new(offset: end_offset, line: 1, column: 1)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
SourceSpan.new(start_pos: start_pos, end_pos: end_pos)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Extract this slice's content from the original input string
|
|
200
|
+
#
|
|
201
|
+
# @param input [String] the original input string
|
|
202
|
+
# @return [String] the slice content extracted from input
|
|
203
|
+
def extract_from(input)
|
|
204
|
+
input.byteslice(offset, length)
|
|
205
|
+
end
|
|
145
206
|
end
|
|
146
207
|
end
|
data/lib/parsanol/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -196,7 +196,6 @@ files:
|
|
|
196
196
|
- lib/parsanol/incremental_parser.rb
|
|
197
197
|
- lib/parsanol/interval_tree.rb
|
|
198
198
|
- lib/parsanol/lazy_result.rb
|
|
199
|
-
- lib/parsanol/lexer.rb
|
|
200
199
|
- lib/parsanol/mermaid.rb
|
|
201
200
|
- lib/parsanol/native.rb
|
|
202
201
|
- lib/parsanol/native/parser.rb
|
|
@@ -210,8 +209,6 @@ files:
|
|
|
210
209
|
- lib/parsanol/optimizers/quantifier_optimizer.rb
|
|
211
210
|
- lib/parsanol/optimizers/sequence_optimizer.rb
|
|
212
211
|
- lib/parsanol/options.rb
|
|
213
|
-
- lib/parsanol/options/ruby_transform.rb
|
|
214
|
-
- lib/parsanol/options/serialized.rb
|
|
215
212
|
- lib/parsanol/options/zero_copy.rb
|
|
216
213
|
- lib/parsanol/parallel.rb
|
|
217
214
|
- lib/parsanol/parser.rb
|
|
@@ -244,7 +241,7 @@ files:
|
|
|
244
241
|
- lib/parsanol/wasm/parsanol.js
|
|
245
242
|
- lib/parsanol/wasm/parslet.d.ts
|
|
246
243
|
- lib/parsanol/wasm_parser.rb
|
|
247
|
-
- parsanol
|
|
244
|
+
- parsanol.gemspec
|
|
248
245
|
homepage: https://github.com/parsanol/parsanol-ruby
|
|
249
246
|
licenses:
|
|
250
247
|
- MIT
|
data/lib/parsanol/lexer.rb
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'parsanol/native'
|
|
4
|
-
|
|
5
|
-
module Parsanol
|
|
6
|
-
# Generic lexer for fast tokenization
|
|
7
|
-
#
|
|
8
|
-
# Create a lexer by subclassing and defining tokens:
|
|
9
|
-
#
|
|
10
|
-
# class JsonLexer < Parsanol::Lexer
|
|
11
|
-
# token :string, /"[^"]*"/
|
|
12
|
-
# token :number, /-?[0-9]+(\.[0-9]+)?/
|
|
13
|
-
# token :true, /true/
|
|
14
|
-
# token :false, /false/
|
|
15
|
-
# token :null, /null/
|
|
16
|
-
# token :lbrace, /\{/
|
|
17
|
-
# token :rbrace, /\}/
|
|
18
|
-
# token :lbracket, /\[/
|
|
19
|
-
# token :rbracket, /\]/
|
|
20
|
-
# token :colon, /:/
|
|
21
|
-
# token :comma, /,/
|
|
22
|
-
#
|
|
23
|
-
# ignore /\s+/
|
|
24
|
-
# end
|
|
25
|
-
#
|
|
26
|
-
# lexer = JsonLexer.new
|
|
27
|
-
# tokens = lexer.tokenize('{"name": "test"}')
|
|
28
|
-
#
|
|
29
|
-
class Lexer
|
|
30
|
-
class << self
|
|
31
|
-
# Define a token pattern
|
|
32
|
-
#
|
|
33
|
-
# @param name [Symbol] Token type name
|
|
34
|
-
# @param pattern [Regexp] Pattern to match
|
|
35
|
-
# @param priority [Integer] Priority for conflict resolution (higher = preferred)
|
|
36
|
-
# @param block [Proc] Optional block to transform the matched value
|
|
37
|
-
def token(name, pattern, priority: 0, &block)
|
|
38
|
-
token_definitions << Definition.new(
|
|
39
|
-
name: name.to_s,
|
|
40
|
-
pattern: pattern.source,
|
|
41
|
-
priority: priority,
|
|
42
|
-
ignore: false,
|
|
43
|
-
transform: block
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Define patterns to ignore (e.g., whitespace, comments)
|
|
48
|
-
#
|
|
49
|
-
# @param pattern [Regexp] Pattern to ignore
|
|
50
|
-
def ignore(pattern)
|
|
51
|
-
token_definitions << Definition.new(
|
|
52
|
-
name: '__ignore__',
|
|
53
|
-
pattern: pattern.source,
|
|
54
|
-
priority: 0,
|
|
55
|
-
ignore: true,
|
|
56
|
-
transform: nil
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Define keywords (identifiers with higher priority)
|
|
61
|
-
#
|
|
62
|
-
# @param keywords [Array<Symbol>] Keyword names
|
|
63
|
-
# @param priority [Integer] Priority (default: 100)
|
|
64
|
-
def keyword(*keywords, priority: 100)
|
|
65
|
-
keywords.each do |kw|
|
|
66
|
-
token_definitions << Definition.new(
|
|
67
|
-
name: kw.to_s.upcase,
|
|
68
|
-
pattern: Regexp.new(Regexp.escape(kw.to_s), Regexp::IGNORECASE).source,
|
|
69
|
-
priority: priority,
|
|
70
|
-
ignore: false,
|
|
71
|
-
transform: nil
|
|
72
|
-
)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Get token definitions for this lexer class
|
|
77
|
-
#
|
|
78
|
-
# @return [Array<Definition>] Token definitions
|
|
79
|
-
def token_definitions
|
|
80
|
-
@token_definitions ||= []
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Inherit token definitions from parent class
|
|
84
|
-
def inherited(subclass)
|
|
85
|
-
super
|
|
86
|
-
subclass.instance_variable_set(:@token_definitions, token_definitions.dup)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Token definition
|
|
91
|
-
Definition = Struct.new(:name, :pattern, :priority, :ignore, :transform)
|
|
92
|
-
|
|
93
|
-
# Initialize the lexer
|
|
94
|
-
def initialize
|
|
95
|
-
@lexer_id = nil
|
|
96
|
-
@transforms = build_transforms
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Tokenize input string
|
|
100
|
-
#
|
|
101
|
-
# @param input [String] Input to tokenize
|
|
102
|
-
# @return [Array<Hash>] Array of tokens with type, value, and location
|
|
103
|
-
def tokenize(input)
|
|
104
|
-
ensure_lexer_created
|
|
105
|
-
|
|
106
|
-
tokens = Native.tokenize_with_lexer(@lexer_id, input)
|
|
107
|
-
|
|
108
|
-
# Apply any transforms
|
|
109
|
-
tokens.map do |token|
|
|
110
|
-
transform = @transforms[token['type']]
|
|
111
|
-
if transform
|
|
112
|
-
token = token.dup
|
|
113
|
-
token['value'] = transform.call(token['value'])
|
|
114
|
-
end
|
|
115
|
-
token
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
private
|
|
120
|
-
|
|
121
|
-
def ensure_lexer_created
|
|
122
|
-
return if @lexer_id
|
|
123
|
-
|
|
124
|
-
definitions = self.class.token_definitions.map do |d|
|
|
125
|
-
{
|
|
126
|
-
'name' => d.name,
|
|
127
|
-
'pattern' => d.pattern,
|
|
128
|
-
'priority' => d.priority,
|
|
129
|
-
'ignore' => d.ignore
|
|
130
|
-
}
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
@lexer_id = Native.create_lexer(definitions)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def build_transforms
|
|
137
|
-
transforms = {}
|
|
138
|
-
self.class.token_definitions.each do |d|
|
|
139
|
-
transforms[d.name] = d.transform if d.transform && !d.ignore
|
|
140
|
-
end
|
|
141
|
-
transforms
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|