moxml 0.1.7 → 0.1.8
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/.github/workflows/dependent-repos.json +5 -0
- data/.github/workflows/dependent-tests.yml +20 -0
- data/.github/workflows/docs.yml +59 -0
- data/.github/workflows/rake.yml +10 -10
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +238 -40
- data/Gemfile +14 -9
- data/LICENSE.md +6 -2
- data/README.adoc +535 -373
- data/Rakefile +53 -0
- data/benchmarks/.gitignore +6 -0
- data/benchmarks/generate_report.rb +550 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +138 -0
- data/docs/_guides/advanced-features.adoc +87 -0
- data/docs/_guides/development-testing.adoc +165 -0
- data/docs/_guides/index.adoc +45 -0
- data/docs/_guides/modifying-xml.adoc +293 -0
- data/docs/_guides/parsing-xml.adoc +231 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +98 -0
- data/docs/_pages/adapters/libxml.adoc +286 -0
- data/docs/_pages/adapters/nokogiri.adoc +252 -0
- data/docs/_pages/adapters/oga.adoc +292 -0
- data/docs/_pages/adapters/ox.adoc +55 -0
- data/docs/_pages/adapters/rexml.adoc +293 -0
- data/docs/_pages/best-practices.adoc +430 -0
- data/docs/_pages/compatibility.adoc +468 -0
- data/docs/_pages/configuration.adoc +251 -0
- data/docs/_pages/error-handling.adoc +350 -0
- data/docs/_pages/headed-ox-limitations.adoc +558 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +141 -0
- data/docs/_pages/node-api-reference.adoc +50 -0
- data/docs/_pages/performance.adoc +36 -0
- data/docs/_pages/quick-start.adoc +244 -0
- data/docs/_pages/thread-safety.adoc +29 -0
- data/docs/_references/document-api.adoc +408 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +268 -0
- data/docs/_tutorials/builder-pattern.adoc +343 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +325 -0
- data/docs/_tutorials/xpath-queries.adoc +359 -0
- data/docs/index.adoc +122 -0
- data/examples/README.md +124 -0
- data/examples/api_client/README.md +424 -0
- data/examples/api_client/api_client.rb +394 -0
- data/examples/api_client/example_response.xml +48 -0
- data/examples/headed_ox_example/README.md +90 -0
- data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
- data/examples/rss_parser/README.md +194 -0
- data/examples/rss_parser/example_feed.xml +93 -0
- data/examples/rss_parser/rss_parser.rb +189 -0
- data/examples/sax_parsing/README.md +50 -0
- data/examples/sax_parsing/data_extractor.rb +75 -0
- data/examples/sax_parsing/example.xml +21 -0
- data/examples/sax_parsing/large_file.rb +78 -0
- data/examples/sax_parsing/simple_parser.rb +55 -0
- data/examples/web_scraper/README.md +352 -0
- data/examples/web_scraper/example_page.html +201 -0
- data/examples/web_scraper/web_scraper.rb +312 -0
- data/lib/moxml/adapter/base.rb +107 -28
- data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
- data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
- data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
- data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
- data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
- data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1548 -0
- data/lib/moxml/adapter/nokogiri.rb +121 -9
- data/lib/moxml/adapter/oga.rb +123 -12
- data/lib/moxml/adapter/ox.rb +282 -26
- data/lib/moxml/adapter/rexml.rb +127 -20
- data/lib/moxml/adapter.rb +21 -4
- data/lib/moxml/attribute.rb +6 -0
- data/lib/moxml/builder.rb +40 -4
- data/lib/moxml/config.rb +8 -3
- data/lib/moxml/context.rb +39 -1
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +39 -6
- data/lib/moxml/document_builder.rb +27 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +94 -3
- data/lib/moxml/node_set.rb +34 -0
- data/lib/moxml/sax/block_handler.rb +194 -0
- data/lib/moxml/sax/element_handler.rb +124 -0
- data/lib/moxml/sax/handler.rb +113 -0
- data/lib/moxml/sax.rb +31 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +4 -4
- data/lib/moxml/xml_utils.rb +7 -4
- data/lib/moxml/xpath/ast/node.rb +159 -0
- data/lib/moxml/xpath/cache.rb +91 -0
- data/lib/moxml/xpath/compiler.rb +1768 -0
- data/lib/moxml/xpath/context.rb +26 -0
- data/lib/moxml/xpath/conversion.rb +124 -0
- data/lib/moxml/xpath/engine.rb +52 -0
- data/lib/moxml/xpath/errors.rb +101 -0
- data/lib/moxml/xpath/lexer.rb +304 -0
- data/lib/moxml/xpath/parser.rb +485 -0
- data/lib/moxml/xpath/ruby/generator.rb +269 -0
- data/lib/moxml/xpath/ruby/node.rb +193 -0
- data/lib/moxml/xpath.rb +37 -0
- data/lib/moxml.rb +5 -2
- data/moxml.gemspec +3 -1
- data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
- data/spec/consistency/README.md +77 -0
- data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
- data/spec/examples/README.md +75 -0
- data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
- data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
- data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
- data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
- data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
- data/spec/integration/README.md +71 -0
- data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
- data/spec/integration/headed_ox_integration_spec.rb +326 -0
- data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
- data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
- data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
- data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
- data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
- data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
- data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
- data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
- data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
- data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
- data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
- data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
- data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
- data/spec/moxml/README.md +41 -0
- data/spec/moxml/adapter/.gitkeep +0 -0
- data/spec/moxml/adapter/README.md +61 -0
- data/spec/moxml/adapter/base_spec.rb +27 -0
- data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
- data/spec/moxml/adapter/libxml_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +9 -8
- data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
- data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
- data/spec/moxml/adapter_spec.rb +16 -0
- data/spec/moxml/attribute_spec.rb +30 -0
- data/spec/moxml/builder_spec.rb +33 -0
- data/spec/moxml/cdata_spec.rb +31 -0
- data/spec/moxml/comment_spec.rb +31 -0
- data/spec/moxml/config_spec.rb +3 -3
- data/spec/moxml/context_spec.rb +28 -0
- data/spec/moxml/declaration_spec.rb +36 -0
- data/spec/moxml/doctype_spec.rb +33 -0
- data/spec/moxml/document_builder_spec.rb +30 -0
- data/spec/moxml/document_spec.rb +105 -0
- data/spec/moxml/element_spec.rb +143 -0
- data/spec/moxml/error_spec.rb +266 -22
- data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
- data/spec/moxml/namespace_spec.rb +32 -0
- data/spec/moxml/node_set_spec.rb +39 -0
- data/spec/moxml/node_spec.rb +37 -0
- data/spec/moxml/processing_instruction_spec.rb +34 -0
- data/spec/moxml/sax_spec.rb +1067 -0
- data/spec/moxml/text_spec.rb +31 -0
- data/spec/moxml/version_spec.rb +14 -0
- data/spec/moxml/xml_utils/.gitkeep +0 -0
- data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
- data/spec/moxml/xml_utils_spec.rb +49 -0
- data/spec/moxml/xpath/ast/node_spec.rb +83 -0
- data/spec/moxml/xpath/axes_spec.rb +296 -0
- data/spec/moxml/xpath/cache_spec.rb +358 -0
- data/spec/moxml/xpath/compiler_spec.rb +406 -0
- data/spec/moxml/xpath/context_spec.rb +210 -0
- data/spec/moxml/xpath/conversion_spec.rb +365 -0
- data/spec/moxml/xpath/fixtures/sample.xml +25 -0
- data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
- data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
- data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
- data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
- data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
- data/spec/moxml/xpath/lexer_spec.rb +488 -0
- data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
- data/spec/moxml/xpath/parser_spec.rb +364 -0
- data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
- data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
- data/spec/moxml/xpath_capabilities_spec.rb +199 -0
- data/spec/moxml/xpath_spec.rb +77 -0
- data/spec/performance/README.md +83 -0
- data/spec/performance/benchmark_spec.rb +64 -0
- data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
- data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
- data/spec/performance/xpath_benchmark_spec.rb +259 -0
- data/spec/spec_helper.rb +58 -1
- data/spec/support/xml_matchers.rb +1 -1
- metadata +176 -34
- data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
- /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
|
@@ -0,0 +1,1768 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module XPath
|
|
5
|
+
# Compiler for transforming XPath AST into executable Ruby code.
|
|
6
|
+
#
|
|
7
|
+
# This class takes an XPath AST (produced by Parser) and compiles it into
|
|
8
|
+
# a Ruby Proc that can be executed against XML documents. The compilation
|
|
9
|
+
# process:
|
|
10
|
+
#
|
|
11
|
+
# 1. Traverse the XPath AST
|
|
12
|
+
# 2. Generate Ruby::Node AST representing Ruby code
|
|
13
|
+
# 3. Use Ruby::Generator to convert to Ruby source string
|
|
14
|
+
# 4. Evaluate source in Context to get a Proc
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# ast = Parser.parse("//book")
|
|
18
|
+
# proc = Compiler.compile_with_cache(ast)
|
|
19
|
+
# result = proc.call(document)
|
|
20
|
+
#
|
|
21
|
+
# @private
|
|
22
|
+
class Compiler
|
|
23
|
+
# Shared context for compiled Procs
|
|
24
|
+
CONTEXT = Context.new
|
|
25
|
+
|
|
26
|
+
# Expression cache
|
|
27
|
+
CACHE = Cache.new
|
|
28
|
+
|
|
29
|
+
# Wildcard for node names/namespace prefixes
|
|
30
|
+
STAR = "*"
|
|
31
|
+
|
|
32
|
+
# Node types that require a NodeSet to push nodes into
|
|
33
|
+
RETURN_NODESET = %i[path absolute_path relative_path axis
|
|
34
|
+
predicate].freeze
|
|
35
|
+
|
|
36
|
+
# Compiles and caches an AST
|
|
37
|
+
#
|
|
38
|
+
# @param ast [AST::Node] XPath AST to compile
|
|
39
|
+
# @param namespaces [Hash, nil] Optional namespace prefix mappings
|
|
40
|
+
# @return [Proc] Compiled Proc that accepts a document
|
|
41
|
+
def self.compile_with_cache(ast, namespaces: nil)
|
|
42
|
+
cache_key = namespaces ? [ast, namespaces] : ast
|
|
43
|
+
CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Initialize compiler
|
|
47
|
+
#
|
|
48
|
+
# @param namespaces [Hash, nil] Optional namespace prefix mappings
|
|
49
|
+
def initialize(namespaces: nil)
|
|
50
|
+
@namespaces = namespaces
|
|
51
|
+
@literal_id = 0
|
|
52
|
+
@predicate_nodesets = []
|
|
53
|
+
@predicate_indexes = []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Compiles an XPath AST into a Ruby Proc
|
|
57
|
+
#
|
|
58
|
+
# @param ast [AST::Node] XPath AST to compile
|
|
59
|
+
# @return [Proc] Executable Proc
|
|
60
|
+
def compile(ast)
|
|
61
|
+
document = literal(:node)
|
|
62
|
+
matched = matched_literal
|
|
63
|
+
context_var = context_literal
|
|
64
|
+
|
|
65
|
+
# Enable debug output
|
|
66
|
+
debug = ENV["DEBUG_XPATH"] == "1"
|
|
67
|
+
if debug
|
|
68
|
+
puts "\n#{'=' * 60}"
|
|
69
|
+
puts "COMPILING XPath"
|
|
70
|
+
puts "=" * 60
|
|
71
|
+
puts "AST: #{ast.inspect}"
|
|
72
|
+
puts
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
ruby_ast = if return_nodeset?(ast)
|
|
76
|
+
process(ast, document) { |node| matched.push(node) }
|
|
77
|
+
else
|
|
78
|
+
process(ast, document)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
proc_ast = literal(:lambda).add_block(document) do
|
|
82
|
+
# Get context from document
|
|
83
|
+
context_assign = context_var.assign(document.context)
|
|
84
|
+
|
|
85
|
+
if return_nodeset?(ast)
|
|
86
|
+
# Create NodeSet using send node: Moxml::NodeSet.new([], context)
|
|
87
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
88
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
89
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
90
|
+
[nodeset_class, "new", empty_array,
|
|
91
|
+
context_var])
|
|
92
|
+
|
|
93
|
+
body = matched.assign(nodeset_new)
|
|
94
|
+
.followed_by(ruby_ast)
|
|
95
|
+
.followed_by(matched)
|
|
96
|
+
else
|
|
97
|
+
body = ruby_ast
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context_assign.followed_by(body)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
generator = Ruby::Generator.new
|
|
104
|
+
source = generator.process(proc_ast)
|
|
105
|
+
|
|
106
|
+
if debug
|
|
107
|
+
puts "GENERATED RUBY CODE:"
|
|
108
|
+
puts "-" * 60
|
|
109
|
+
puts source
|
|
110
|
+
puts "=" * 60
|
|
111
|
+
puts
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
CONTEXT.evaluate(source)
|
|
115
|
+
ensure
|
|
116
|
+
@literal_id = 0
|
|
117
|
+
@predicate_nodesets.clear
|
|
118
|
+
@predicate_indexes.clear
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Process a single XPath AST node
|
|
122
|
+
#
|
|
123
|
+
# @param ast [AST::Node] AST node to process
|
|
124
|
+
# @param input [Ruby::Node] Input node
|
|
125
|
+
# @yield [Ruby::Node] Yields matched nodes if block given
|
|
126
|
+
# @return [Ruby::Node] Ruby AST node
|
|
127
|
+
def process(ast, input, &block)
|
|
128
|
+
send(:"on_#{ast.type}", ast, input, &block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Dispatcher for generic binary operator nodes
|
|
132
|
+
def on_binary_op(ast, input, &block)
|
|
133
|
+
operator = ast.value # :eq, :lt, :add, :plus, :star, etc.
|
|
134
|
+
|
|
135
|
+
# Map token names to handler method names
|
|
136
|
+
method_name = case operator
|
|
137
|
+
when :plus then :add
|
|
138
|
+
when :minus then :sub
|
|
139
|
+
when :star then :mul
|
|
140
|
+
else operator # eq, lt, gt, div, mod, etc.
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
send(:"on_#{method_name}", ast, input, &block)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Dispatcher for generic unary operator nodes
|
|
147
|
+
def on_unary_op(ast, input, &block)
|
|
148
|
+
operator = ast.value # :minus
|
|
149
|
+
send(:"on_#{operator}", ast, input, &block)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Dispatcher for union nodes (parser creates :union, compiler uses :pipe)
|
|
153
|
+
def on_union(ast, input, &block)
|
|
154
|
+
on_pipe(ast, input, &block)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Helper methods for creating Ruby AST nodes
|
|
160
|
+
|
|
161
|
+
def literal(value)
|
|
162
|
+
case value
|
|
163
|
+
when Symbol, String
|
|
164
|
+
end
|
|
165
|
+
Ruby::Node.new(:lit, [value.to_s])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Create a constant reference like Moxml::Document
|
|
169
|
+
def const_ref(*parts)
|
|
170
|
+
Ruby::Node.new(:const, parts)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def unique_literal(name)
|
|
174
|
+
@literal_id += 1
|
|
175
|
+
literal("#{name}#{@literal_id}")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def string(value)
|
|
179
|
+
Ruby::Node.new(:string, [value.to_s])
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def symbol(value)
|
|
183
|
+
Ruby::Node.new(:symbol, [value.to_sym])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def matched_literal
|
|
187
|
+
literal(:matched)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def context_literal
|
|
191
|
+
literal(:context)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self_nil
|
|
195
|
+
@self_nil ||= literal(:nil)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self_true
|
|
199
|
+
@self_true ||= literal(true)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self_false
|
|
203
|
+
@self_false ||= literal(false)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def return_nodeset?(ast)
|
|
207
|
+
# Special cases where relative_path returns node directly:
|
|
208
|
+
# - "." (current node)
|
|
209
|
+
# - ".." (parent node)
|
|
210
|
+
if ast.type == :relative_path && ast.children.size == 1
|
|
211
|
+
child_type = ast.children[0].type
|
|
212
|
+
return false if %i[current parent].include?(child_type)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
RETURN_NODESET.include?(ast.type)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Type checking helpers
|
|
219
|
+
|
|
220
|
+
def document_or_node(node)
|
|
221
|
+
doc_class = const_ref("Moxml", "Document")
|
|
222
|
+
node_class = const_ref("Moxml", "Node")
|
|
223
|
+
node.is_a?(doc_class).or(node.is_a?(node_class))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def element_or_attribute(node)
|
|
227
|
+
elem_class = const_ref("Moxml", "Element")
|
|
228
|
+
attr_class = const_ref("Moxml", "Attribute")
|
|
229
|
+
node.is_a?(elem_class).or(node.is_a?(attr_class))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def attribute_or_node(node)
|
|
233
|
+
attr_class = const_ref("Moxml", "Attribute")
|
|
234
|
+
node_class = const_ref("Moxml", "Node")
|
|
235
|
+
node.is_a?(attr_class).or(node.is_a?(node_class))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Path handling
|
|
239
|
+
|
|
240
|
+
# Handle absolute paths like /root or //descendant
|
|
241
|
+
def on_absolute_path(ast, input, &block)
|
|
242
|
+
if ast.children.empty?
|
|
243
|
+
# Just "/" - return the document/root
|
|
244
|
+
yield input if block
|
|
245
|
+
input
|
|
246
|
+
else
|
|
247
|
+
# Process steps from the input (which should be a document)
|
|
248
|
+
# Don't call input.root - that would skip a level
|
|
249
|
+
first_child = ast.children[0]
|
|
250
|
+
|
|
251
|
+
# For absolute paths, we process from the document itself
|
|
252
|
+
if ast.children.size == 1
|
|
253
|
+
process(first_child, input, &block)
|
|
254
|
+
else
|
|
255
|
+
# Multiple steps - create a path
|
|
256
|
+
path_node = AST::Node.new(:path, ast.children)
|
|
257
|
+
process(path_node, input, &block)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Handle relative paths
|
|
263
|
+
def on_relative_path(ast, input, &block)
|
|
264
|
+
on_path(ast, input, &block)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Handle path (series of steps)
|
|
268
|
+
def on_path(ast, input, &block)
|
|
269
|
+
return input if ast.children.empty?
|
|
270
|
+
|
|
271
|
+
# First step from input
|
|
272
|
+
first_step = ast.children[0]
|
|
273
|
+
|
|
274
|
+
if ast.children.size == 1
|
|
275
|
+
# Single step
|
|
276
|
+
process(first_step, input, &block)
|
|
277
|
+
else
|
|
278
|
+
# Multiple steps - need to accumulate results
|
|
279
|
+
temp_results = unique_literal(:temp_results)
|
|
280
|
+
context_var = context_literal
|
|
281
|
+
|
|
282
|
+
# Create NodeSet for temp results
|
|
283
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
284
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
285
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
286
|
+
[nodeset_class, "new", empty_array,
|
|
287
|
+
context_var])
|
|
288
|
+
|
|
289
|
+
temp_results.assign(nodeset_new)
|
|
290
|
+
.followed_by do
|
|
291
|
+
process(first_step, input) do |node|
|
|
292
|
+
temp_results.push(node)
|
|
293
|
+
end
|
|
294
|
+
.followed_by do
|
|
295
|
+
# Process remaining steps on each result
|
|
296
|
+
remaining_steps = AST::Node.new(:path, ast.children[1..])
|
|
297
|
+
temp_node = unique_literal(:temp_node)
|
|
298
|
+
|
|
299
|
+
temp_results.each.add_block(temp_node) do
|
|
300
|
+
process(remaining_steps, temp_node, &block)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Axis handling
|
|
308
|
+
|
|
309
|
+
# Dispatch axes to specific handlers
|
|
310
|
+
def on_axis(ast, input, &block)
|
|
311
|
+
axis_name, test, *_predicates = ast.children
|
|
312
|
+
|
|
313
|
+
handler = axis_name.gsub("-", "_")
|
|
314
|
+
|
|
315
|
+
send(:"on_axis_#{handler}", test, input, &block)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Handle step with predicates (created by parser)
|
|
319
|
+
def on_step_with_predicates(ast, input, &block)
|
|
320
|
+
step, *predicates = ast.children
|
|
321
|
+
|
|
322
|
+
# If no predicates, just process the step
|
|
323
|
+
return process(step, input, &block) if predicates.empty?
|
|
324
|
+
|
|
325
|
+
# Build predicate chain: step -> pred1 -> pred2 -> ...
|
|
326
|
+
# Each predicate wraps the previous result as its test
|
|
327
|
+
result_ast = step
|
|
328
|
+
|
|
329
|
+
predicates.each do |pred_wrapper|
|
|
330
|
+
# pred_wrapper is :predicate node with children [expression]
|
|
331
|
+
# Build proper :predicate node with [test, expression, nil]
|
|
332
|
+
predicate_expr = pred_wrapper.children[0]
|
|
333
|
+
result_ast = AST::Node.new(:predicate,
|
|
334
|
+
[result_ast, predicate_expr, nil])
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Process the final chained AST
|
|
338
|
+
process(result_ast, input, &block)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# AXIS: child - direct children
|
|
342
|
+
def on_axis_child(ast, input)
|
|
343
|
+
child = unique_literal(:child)
|
|
344
|
+
|
|
345
|
+
document_or_node(input).if_true do
|
|
346
|
+
input.children.each.add_block(child) do
|
|
347
|
+
condition = process(ast, child)
|
|
348
|
+
if block_given?
|
|
349
|
+
condition.if_true { yield child }
|
|
350
|
+
else
|
|
351
|
+
condition.if_true { child }
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# AXIS: self - the node itself
|
|
358
|
+
def on_axis_self(ast, input)
|
|
359
|
+
condition = process(ast, input)
|
|
360
|
+
if block_given?
|
|
361
|
+
condition.if_true { yield input }
|
|
362
|
+
else
|
|
363
|
+
condition.if_true { input }
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# AXIS: parent - parent node
|
|
368
|
+
def on_axis_parent(ast, input)
|
|
369
|
+
parent = unique_literal(:parent)
|
|
370
|
+
|
|
371
|
+
attribute_or_node(input).if_true do
|
|
372
|
+
parent.assign(input.parent).followed_by do
|
|
373
|
+
condition = process(ast, parent)
|
|
374
|
+
if block_given?
|
|
375
|
+
condition.if_true { yield parent }
|
|
376
|
+
else
|
|
377
|
+
condition.if_true { parent }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# AXIS: descendant-or-self - Enables // operator
|
|
384
|
+
def on_axis_descendant_or_self(ast, input)
|
|
385
|
+
node = unique_literal(:descendant)
|
|
386
|
+
doc_class = const_ref("Moxml", "Document")
|
|
387
|
+
|
|
388
|
+
document_or_node(input).if_true do
|
|
389
|
+
# Create a proper if-else structure that prevents double traversal
|
|
390
|
+
input.is_a?(doc_class).if_true do
|
|
391
|
+
# DOCUMENT PATH: test root, then traverse from root
|
|
392
|
+
root = unique_literal(:root)
|
|
393
|
+
root.assign(input.root).followed_by do
|
|
394
|
+
root.if_true do
|
|
395
|
+
# Test root first
|
|
396
|
+
condition = process(ast, root)
|
|
397
|
+
(if block_given?
|
|
398
|
+
condition.if_true { yield root }
|
|
399
|
+
else
|
|
400
|
+
condition.if_true { root }
|
|
401
|
+
end)
|
|
402
|
+
.followed_by do
|
|
403
|
+
# Traverse descendants FROM root only (not document.each_node)
|
|
404
|
+
root.each_node.add_block(node) do
|
|
405
|
+
desc_condition = process(ast, node)
|
|
406
|
+
if block_given?
|
|
407
|
+
desc_condition.if_true { yield node }
|
|
408
|
+
else
|
|
409
|
+
desc_condition.if_true { node }
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end.else do
|
|
416
|
+
# NON-DOCUMENT PATH: test self, then traverse from self
|
|
417
|
+
condition = process(ast, input)
|
|
418
|
+
(if block_given?
|
|
419
|
+
condition.if_true { yield input }
|
|
420
|
+
else
|
|
421
|
+
condition.if_true { input }
|
|
422
|
+
end)
|
|
423
|
+
.followed_by do
|
|
424
|
+
# Traverse descendants FROM input
|
|
425
|
+
input.each_node.add_block(node) do
|
|
426
|
+
desc_condition = process(ast, node)
|
|
427
|
+
if block_given?
|
|
428
|
+
desc_condition.if_true { yield node }
|
|
429
|
+
else
|
|
430
|
+
desc_condition.if_true { node }
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# AXIS: attribute - Enables @attribute syntax
|
|
439
|
+
def on_axis_attribute(ast, input)
|
|
440
|
+
elem_class = const_ref("Moxml", "Element")
|
|
441
|
+
attribute = unique_literal(:attribute)
|
|
442
|
+
|
|
443
|
+
input.is_a?(elem_class).if_true do
|
|
444
|
+
input.attributes.each.add_block(attribute) do
|
|
445
|
+
# Use process to handle both :test and :wildcard nodes
|
|
446
|
+
condition = process(ast, attribute)
|
|
447
|
+
|
|
448
|
+
if block_given?
|
|
449
|
+
condition.if_true { yield attribute }
|
|
450
|
+
else
|
|
451
|
+
condition.if_true { attribute }
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# AXIS: descendant - All descendant nodes (without self)
|
|
458
|
+
def on_axis_descendant(ast, input)
|
|
459
|
+
node = unique_literal(:descendant)
|
|
460
|
+
|
|
461
|
+
document_or_node(input).if_true do
|
|
462
|
+
input.each_node.add_block(node) do
|
|
463
|
+
condition = process(ast, node)
|
|
464
|
+
if block_given?
|
|
465
|
+
condition.if_true { yield node }
|
|
466
|
+
else
|
|
467
|
+
condition.if_true { node }
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Helper: Recursively traverse all descendants
|
|
474
|
+
def traverse_all_descendants(input, &block)
|
|
475
|
+
child = unique_literal(:child)
|
|
476
|
+
|
|
477
|
+
input.children.each.add_block(child) do
|
|
478
|
+
# Yield this child
|
|
479
|
+
yield child
|
|
480
|
+
# Then recursively traverse its descendants
|
|
481
|
+
traverse_all_descendants(child, &block)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Node test handling
|
|
486
|
+
|
|
487
|
+
# Handle node tests (name matching)
|
|
488
|
+
def on_test(ast, input)
|
|
489
|
+
condition = element_or_attribute(input)
|
|
490
|
+
name_match = match_name_and_namespace(ast, input)
|
|
491
|
+
|
|
492
|
+
name_match ? condition.and(name_match) : condition
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Handle wildcard test (*)
|
|
496
|
+
def on_wildcard(_ast, input)
|
|
497
|
+
element_or_attribute(input)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Match element/attribute names and namespaces
|
|
501
|
+
def match_name_and_namespace(ast, input)
|
|
502
|
+
ns = ast.value[:namespace]
|
|
503
|
+
name = ast.value[:name]
|
|
504
|
+
|
|
505
|
+
# Wildcard for both name and namespace means match all - return nil
|
|
506
|
+
# nil means "no additional constraint beyond type check"
|
|
507
|
+
return nil if name == STAR && (!ns || ns == STAR)
|
|
508
|
+
|
|
509
|
+
condition = nil
|
|
510
|
+
name_str = string(name)
|
|
511
|
+
zero = literal(0)
|
|
512
|
+
|
|
513
|
+
# Match name (case-insensitive) unless wildcard
|
|
514
|
+
if name != STAR
|
|
515
|
+
# If we have a namespace prefix, we need to compare local names
|
|
516
|
+
# For elements like "ns:item", we should compare against "item" not "ns:item"
|
|
517
|
+
if ns && ns != STAR && @namespaces && @namespaces[ns]
|
|
518
|
+
# Extract local name by splitting on ':' and taking the last part
|
|
519
|
+
# This handles both "ns:item" -> "item" and "item" -> "item"
|
|
520
|
+
local_name_expr = input.name.split(string(":")).last
|
|
521
|
+
condition = local_name_expr.eq(name_str)
|
|
522
|
+
.or(local_name_expr.casecmp(name_str).eq(zero))
|
|
523
|
+
else
|
|
524
|
+
# No namespace or no mapping - compare full name
|
|
525
|
+
condition = input.name.eq(name_str)
|
|
526
|
+
.or(input.name.casecmp(name_str).eq(zero))
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Match namespace if specified
|
|
531
|
+
if ns && ns != STAR
|
|
532
|
+
if @namespaces && @namespaces[ns]
|
|
533
|
+
# Resolve prefix to URI using namespace mappings
|
|
534
|
+
ns_uri = @namespaces[ns]
|
|
535
|
+
ns_match = input.namespace.and(input.namespace.uri.eq(string(ns_uri)))
|
|
536
|
+
else
|
|
537
|
+
# No mapping provided - check against element's namespace prefix
|
|
538
|
+
# Need to ensure input.namespace exists first
|
|
539
|
+
ns_match = input.namespace.and(input.namespace.prefix.eq(string(ns)))
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
condition = condition ? condition.and(ns_match) : ns_match
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
condition
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Literal value handling
|
|
549
|
+
|
|
550
|
+
# String literals
|
|
551
|
+
def on_string(ast, *)
|
|
552
|
+
string(ast.value)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Number literals (both int and float)
|
|
556
|
+
def on_number(ast, *)
|
|
557
|
+
literal(ast.value.to_f.to_s)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Current node (.)
|
|
561
|
+
def on_current(_ast, input)
|
|
562
|
+
if block_given?
|
|
563
|
+
yield input # Block returns Ruby::Node for matched.push(input)
|
|
564
|
+
else
|
|
565
|
+
input
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Parent node (..)
|
|
570
|
+
def on_parent(_ast, input)
|
|
571
|
+
input.parent
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# ===== OPERATORS =====
|
|
575
|
+
|
|
576
|
+
# Comparison: = (equality)
|
|
577
|
+
def on_eq(ast, input, &block)
|
|
578
|
+
conv = literal(Moxml::XPath::Conversion)
|
|
579
|
+
|
|
580
|
+
operator(ast, input) do |left, right|
|
|
581
|
+
mass_assign([left, right], conv.to_compatible_types(left, right))
|
|
582
|
+
.followed_by do
|
|
583
|
+
operation = left.eq(right)
|
|
584
|
+
|
|
585
|
+
block ? operation.if_true(&block) : operation
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Comparison: != (inequality)
|
|
591
|
+
def on_neq(ast, input, &block)
|
|
592
|
+
conv = literal(Moxml::XPath::Conversion)
|
|
593
|
+
|
|
594
|
+
operator(ast, input) do |left, right|
|
|
595
|
+
mass_assign([left, right], conv.to_compatible_types(left, right))
|
|
596
|
+
.followed_by do
|
|
597
|
+
operation = left != right
|
|
598
|
+
|
|
599
|
+
block ? operation.if_true(&block) : operation
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Comparison: < (less than)
|
|
605
|
+
def on_lt(ast, input, &block)
|
|
606
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
607
|
+
|
|
608
|
+
operator(ast, input) do |left, right|
|
|
609
|
+
lval = conversion.to_float(left)
|
|
610
|
+
rval = conversion.to_float(right)
|
|
611
|
+
operation = lval < rval
|
|
612
|
+
|
|
613
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Comparison: > (greater than)
|
|
618
|
+
def on_gt(ast, input, &block)
|
|
619
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
620
|
+
|
|
621
|
+
operator(ast, input) do |left, right|
|
|
622
|
+
lval = conversion.to_float(left)
|
|
623
|
+
rval = conversion.to_float(right)
|
|
624
|
+
operation = lval > rval
|
|
625
|
+
|
|
626
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# Comparison: <= (less than or equal)
|
|
631
|
+
def on_lte(ast, input, &block)
|
|
632
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
633
|
+
|
|
634
|
+
operator(ast, input) do |left, right|
|
|
635
|
+
lval = conversion.to_float(left)
|
|
636
|
+
rval = conversion.to_float(right)
|
|
637
|
+
operation = lval <= rval
|
|
638
|
+
|
|
639
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Comparison: >= (greater than or equal)
|
|
644
|
+
def on_gte(ast, input, &block)
|
|
645
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
646
|
+
|
|
647
|
+
operator(ast, input) do |left, right|
|
|
648
|
+
lval = conversion.to_float(left)
|
|
649
|
+
rval = conversion.to_float(right)
|
|
650
|
+
operation = lval >= rval
|
|
651
|
+
|
|
652
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Arithmetic: + (addition)
|
|
657
|
+
def on_add(ast, input, &block)
|
|
658
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
659
|
+
|
|
660
|
+
operator(ast, input) do |left, right|
|
|
661
|
+
lval = conversion.to_float(left)
|
|
662
|
+
rval = conversion.to_float(right)
|
|
663
|
+
operation = lval + rval
|
|
664
|
+
|
|
665
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Arithmetic: - (subtraction)
|
|
670
|
+
def on_sub(ast, input, &block)
|
|
671
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
672
|
+
|
|
673
|
+
operator(ast, input) do |left, right|
|
|
674
|
+
lval = conversion.to_float(left)
|
|
675
|
+
rval = conversion.to_float(right)
|
|
676
|
+
operation = lval - rval
|
|
677
|
+
|
|
678
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Arithmetic: * (multiplication)
|
|
683
|
+
def on_mul(ast, input, &block)
|
|
684
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
685
|
+
|
|
686
|
+
operator(ast, input) do |left, right|
|
|
687
|
+
lval = conversion.to_float(left)
|
|
688
|
+
rval = conversion.to_float(right)
|
|
689
|
+
operation = lval * rval
|
|
690
|
+
|
|
691
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Arithmetic: div (division)
|
|
696
|
+
def on_div(ast, input, &block)
|
|
697
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
698
|
+
|
|
699
|
+
operator(ast, input) do |left, right|
|
|
700
|
+
lval = conversion.to_float(left)
|
|
701
|
+
rval = conversion.to_float(right)
|
|
702
|
+
operation = lval / rval
|
|
703
|
+
|
|
704
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Arithmetic: mod (modulo)
|
|
709
|
+
def on_mod(ast, input, &block)
|
|
710
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
711
|
+
|
|
712
|
+
operator(ast, input) do |left, right|
|
|
713
|
+
lval = conversion.to_float(left)
|
|
714
|
+
rval = conversion.to_float(right)
|
|
715
|
+
operation = lval % rval
|
|
716
|
+
|
|
717
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Unary: minus (negation)
|
|
722
|
+
def on_minus(ast, input, &block)
|
|
723
|
+
operand = ast.children[0]
|
|
724
|
+
operand_ast = process(operand, input)
|
|
725
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
726
|
+
|
|
727
|
+
operand_var = unique_literal(:unary_operand)
|
|
728
|
+
operand_var.assign(operand_ast)
|
|
729
|
+
.followed_by do
|
|
730
|
+
negated = literal(0) - conversion.to_float(operand_var)
|
|
731
|
+
block ? conversion.to_boolean(negated).if_true(&block) : negated
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Logical: and
|
|
736
|
+
def on_and(ast, input, &block)
|
|
737
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
738
|
+
|
|
739
|
+
operator(ast, input) do |left, right|
|
|
740
|
+
lval = conversion.to_boolean(left)
|
|
741
|
+
rval = conversion.to_boolean(right)
|
|
742
|
+
operation = lval.and(rval)
|
|
743
|
+
|
|
744
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Logical: or
|
|
749
|
+
def on_or(ast, input, &block)
|
|
750
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
751
|
+
|
|
752
|
+
operator(ast, input) do |left, right|
|
|
753
|
+
lval = conversion.to_boolean(left)
|
|
754
|
+
rval = conversion.to_boolean(right)
|
|
755
|
+
operation = lval.or(rval)
|
|
756
|
+
|
|
757
|
+
block ? conversion.to_boolean(operation).if_true(&block) : operation
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Union: | (pipe)
|
|
762
|
+
def on_pipe(ast, input)
|
|
763
|
+
left, right = ast.children
|
|
764
|
+
|
|
765
|
+
union = unique_literal(:union)
|
|
766
|
+
context_var = context_literal
|
|
767
|
+
|
|
768
|
+
# Create NodeSet for union results
|
|
769
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
770
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
771
|
+
|
|
772
|
+
# Expressions such as "a | b | c"
|
|
773
|
+
if left.type == :pipe
|
|
774
|
+
union.assign(process(left, input))
|
|
775
|
+
.followed_by(process(right, input) { |node| union << node })
|
|
776
|
+
.followed_by(union)
|
|
777
|
+
# Expressions such as "a | b"
|
|
778
|
+
else
|
|
779
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
780
|
+
[nodeset_class, "new", empty_array,
|
|
781
|
+
context_var])
|
|
782
|
+
|
|
783
|
+
union.assign(nodeset_new)
|
|
784
|
+
.followed_by(process(left, input) { |node| union << node })
|
|
785
|
+
.followed_by(process(right, input) { |node| union << node })
|
|
786
|
+
.followed_by(union)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Variable: $variable
|
|
791
|
+
def on_var(ast, *)
|
|
792
|
+
name = ast.children[0]
|
|
793
|
+
|
|
794
|
+
variables_literal.and(variables_literal[string(name)])
|
|
795
|
+
.or(send_message(:raise, string("Undefined XPath variable: #{name}")))
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# Predicate handling: //book[@price < 20]
|
|
799
|
+
def on_predicate(ast, input, &block)
|
|
800
|
+
test, predicate, following = ast.children
|
|
801
|
+
|
|
802
|
+
index_var = unique_literal(:index)
|
|
803
|
+
|
|
804
|
+
# Check predicate type to determine strategy
|
|
805
|
+
method = if number?(predicate)
|
|
806
|
+
:on_predicate_index
|
|
807
|
+
elsif has_call_node?(predicate, "last")
|
|
808
|
+
:on_predicate_temporary
|
|
809
|
+
else
|
|
810
|
+
:on_predicate_direct
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
@predicate_indexes << index_var
|
|
814
|
+
|
|
815
|
+
result = index_var.assign(literal(1)).followed_by do
|
|
816
|
+
send(method, input, test, predicate) do |matched|
|
|
817
|
+
if following
|
|
818
|
+
process(following, matched, &block)
|
|
819
|
+
else
|
|
820
|
+
yield matched
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
@predicate_indexes.pop
|
|
826
|
+
|
|
827
|
+
result
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
# Predicate that requires temporary NodeSet (for last())
|
|
831
|
+
def on_predicate_temporary(input, test, predicate)
|
|
832
|
+
temp_set = unique_literal(:temp_set)
|
|
833
|
+
pred_node = unique_literal(:pred_node)
|
|
834
|
+
pred_var = unique_literal(:pred_var)
|
|
835
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
836
|
+
context_var = context_literal
|
|
837
|
+
|
|
838
|
+
index_var = predicate_index
|
|
839
|
+
index_step = literal(1)
|
|
840
|
+
|
|
841
|
+
@predicate_nodesets << temp_set
|
|
842
|
+
|
|
843
|
+
# Create NodeSet for temp results
|
|
844
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
845
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
846
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
847
|
+
[nodeset_class, "new", empty_array,
|
|
848
|
+
context_var])
|
|
849
|
+
|
|
850
|
+
ast = temp_set.assign(nodeset_new)
|
|
851
|
+
.followed_by do
|
|
852
|
+
process(test, input) { |node| temp_set << node }
|
|
853
|
+
end
|
|
854
|
+
.followed_by do
|
|
855
|
+
temp_set.each.add_block(pred_node) do
|
|
856
|
+
pred_ast = process(predicate, pred_node)
|
|
857
|
+
|
|
858
|
+
pred_var.assign(pred_ast)
|
|
859
|
+
.followed_by do
|
|
860
|
+
pred_var.is_a?(literal(:Numeric)).if_true do
|
|
861
|
+
pred_var.assign(pred_var.to_i.eq(index_var))
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
.followed_by do
|
|
865
|
+
conversion.to_boolean(pred_var).if_true { yield pred_node }
|
|
866
|
+
end
|
|
867
|
+
.followed_by do
|
|
868
|
+
index_var.assign(index_var + index_step)
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
@predicate_nodesets.pop
|
|
874
|
+
|
|
875
|
+
ast
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# Predicate that doesn't require temporary NodeSet
|
|
879
|
+
def on_predicate_direct(input, test, predicate)
|
|
880
|
+
pred_var = unique_literal(:pred_var)
|
|
881
|
+
index_var = predicate_index
|
|
882
|
+
index_step = literal(1)
|
|
883
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
884
|
+
|
|
885
|
+
process(test, input) do |matched_test_node|
|
|
886
|
+
pred_ast = if return_nodeset?(predicate)
|
|
887
|
+
# Use catch/throw for early return
|
|
888
|
+
catch_message(:predicate_matched) do
|
|
889
|
+
process(predicate, matched_test_node) do
|
|
890
|
+
throw_message(:predicate_matched, self_true)
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
else
|
|
894
|
+
process(predicate, matched_test_node)
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
pred_var.assign(pred_ast)
|
|
898
|
+
.followed_by do
|
|
899
|
+
pred_var.is_a?(literal(:Numeric)).if_true do
|
|
900
|
+
pred_var.assign(pred_var.to_i.eq(index_var))
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
.followed_by do
|
|
904
|
+
conversion.to_boolean(pred_var).if_true do
|
|
905
|
+
yield matched_test_node
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
.followed_by do
|
|
909
|
+
index_var.assign(index_var + index_step)
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# Predicate with literal index: //book[1]
|
|
915
|
+
def on_predicate_index(input, test, predicate)
|
|
916
|
+
index_var = predicate_index
|
|
917
|
+
index_step = literal(1)
|
|
918
|
+
|
|
919
|
+
index = process(predicate, input).to_i
|
|
920
|
+
|
|
921
|
+
process(test, input) do |matched_test_node|
|
|
922
|
+
index_var.eq(index)
|
|
923
|
+
.if_true do
|
|
924
|
+
yield matched_test_node
|
|
925
|
+
end
|
|
926
|
+
.followed_by do
|
|
927
|
+
index_var.assign(index_var + index_step)
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# ===== XPATH FUNCTIONS =====
|
|
933
|
+
|
|
934
|
+
# XPath function dispatcher
|
|
935
|
+
def on_call(ast, input, &block)
|
|
936
|
+
# Function name is stored in value field, not children
|
|
937
|
+
name = ast.value
|
|
938
|
+
args = ast.children
|
|
939
|
+
|
|
940
|
+
handler = name.to_s.gsub("-", "_")
|
|
941
|
+
|
|
942
|
+
send(:"on_call_#{handler}", input, *args, &block)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# Alias for function nodes (parser creates :function, compiler uses on_call)
|
|
946
|
+
alias on_function on_call
|
|
947
|
+
|
|
948
|
+
# 1. string() - Convert value to string
|
|
949
|
+
def on_call_string(input, arg = nil)
|
|
950
|
+
convert_var = unique_literal(:convert)
|
|
951
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
952
|
+
|
|
953
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
954
|
+
convert_var.assign(conversion.to_string(arg_var))
|
|
955
|
+
.followed_by do
|
|
956
|
+
if block_given?
|
|
957
|
+
convert_var.empty?.if_false { yield convert_var }
|
|
958
|
+
else
|
|
959
|
+
convert_var
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# 2. concat() - Concatenate strings
|
|
966
|
+
def on_call_concat(input, *args)
|
|
967
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
968
|
+
assigns = []
|
|
969
|
+
conversions = []
|
|
970
|
+
|
|
971
|
+
args.each do |arg|
|
|
972
|
+
arg_var = unique_literal(:concat_arg)
|
|
973
|
+
arg_ast = try_match_first_node(arg, input)
|
|
974
|
+
|
|
975
|
+
assigns << arg_var.assign(arg_ast)
|
|
976
|
+
conversions << conversion.to_string(arg_var)
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
concatted = assigns.inject(:followed_by)
|
|
980
|
+
.followed_by(conversions.inject(:+))
|
|
981
|
+
|
|
982
|
+
block_given? ? concatted.empty?.if_false { yield concatted } : concatted
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# 3. starts-with() - Check string prefix
|
|
986
|
+
def on_call_starts_with(input, haystack, needle)
|
|
987
|
+
haystack_var = unique_literal(:haystack)
|
|
988
|
+
needle_var = unique_literal(:needle)
|
|
989
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
990
|
+
|
|
991
|
+
haystack_var.assign(try_match_first_node(haystack, input))
|
|
992
|
+
.followed_by do
|
|
993
|
+
needle_var.assign(try_match_first_node(needle, input))
|
|
994
|
+
end
|
|
995
|
+
.followed_by do
|
|
996
|
+
haystack_var.assign(conversion.to_string(haystack_var))
|
|
997
|
+
.followed_by do
|
|
998
|
+
needle_var.assign(conversion.to_string(needle_var))
|
|
999
|
+
end
|
|
1000
|
+
.followed_by do
|
|
1001
|
+
equal = needle_var.empty?
|
|
1002
|
+
.or(haystack_var.start_with?(needle_var))
|
|
1003
|
+
|
|
1004
|
+
block_given? ? equal.if_true { yield equal } : equal
|
|
1005
|
+
end
|
|
1006
|
+
end
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# 4. contains() - Check substring
|
|
1010
|
+
def on_call_contains(input, haystack, needle)
|
|
1011
|
+
haystack_lit = unique_literal(:haystack)
|
|
1012
|
+
needle_lit = unique_literal(:needle)
|
|
1013
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1014
|
+
|
|
1015
|
+
haystack_lit.assign(try_match_first_node(haystack, input))
|
|
1016
|
+
.followed_by do
|
|
1017
|
+
needle_lit.assign(try_match_first_node(needle, input))
|
|
1018
|
+
end
|
|
1019
|
+
.followed_by do
|
|
1020
|
+
converted = conversion.to_string(haystack_lit)
|
|
1021
|
+
.include?(conversion.to_string(needle_lit))
|
|
1022
|
+
|
|
1023
|
+
block_given? ? converted.if_true { yield converted } : converted
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
# 5. substring-before() - Get part before separator
|
|
1028
|
+
def on_call_substring_before(input, haystack, needle)
|
|
1029
|
+
haystack_var = unique_literal(:haystack)
|
|
1030
|
+
needle_var = unique_literal(:needle)
|
|
1031
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1032
|
+
|
|
1033
|
+
before = unique_literal(:before)
|
|
1034
|
+
sep = unique_literal(:sep)
|
|
1035
|
+
after = unique_literal(:after)
|
|
1036
|
+
|
|
1037
|
+
haystack_var.assign(try_match_first_node(haystack, input))
|
|
1038
|
+
.followed_by do
|
|
1039
|
+
needle_var.assign(try_match_first_node(needle, input))
|
|
1040
|
+
end
|
|
1041
|
+
.followed_by do
|
|
1042
|
+
converted = conversion.to_string(haystack_var)
|
|
1043
|
+
.partition(conversion.to_string(needle_var))
|
|
1044
|
+
|
|
1045
|
+
mass_assign([before, sep, after], converted).followed_by do
|
|
1046
|
+
sep.empty?
|
|
1047
|
+
.if_true { sep }
|
|
1048
|
+
.else { block_given? ? yield : before }
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# 6. substring-after() - Get part after separator
|
|
1054
|
+
def on_call_substring_after(input, haystack, needle)
|
|
1055
|
+
haystack_var = unique_literal(:haystack)
|
|
1056
|
+
needle_var = unique_literal(:needle)
|
|
1057
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1058
|
+
|
|
1059
|
+
before = unique_literal(:before)
|
|
1060
|
+
sep = unique_literal(:sep)
|
|
1061
|
+
after = unique_literal(:after)
|
|
1062
|
+
|
|
1063
|
+
haystack_var.assign(try_match_first_node(haystack, input))
|
|
1064
|
+
.followed_by do
|
|
1065
|
+
needle_var.assign(try_match_first_node(needle, input))
|
|
1066
|
+
end
|
|
1067
|
+
.followed_by do
|
|
1068
|
+
converted = conversion.to_string(haystack_var)
|
|
1069
|
+
.partition(conversion.to_string(needle_var))
|
|
1070
|
+
|
|
1071
|
+
mass_assign([before, sep, after], converted).followed_by do
|
|
1072
|
+
sep.empty?
|
|
1073
|
+
.if_true { sep }
|
|
1074
|
+
.else { block_given? ? yield : after }
|
|
1075
|
+
end
|
|
1076
|
+
end
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
# 7. substring() - Extract substring
|
|
1080
|
+
def on_call_substring(input, haystack, start, length = nil)
|
|
1081
|
+
haystack_var = unique_literal(:haystack)
|
|
1082
|
+
start_var = unique_literal(:start)
|
|
1083
|
+
length_var = unique_literal(:length)
|
|
1084
|
+
result_var = unique_literal(:result)
|
|
1085
|
+
ruby_start = unique_literal(:ruby_start)
|
|
1086
|
+
effective_length = unique_literal(:effective_length)
|
|
1087
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1088
|
+
|
|
1089
|
+
haystack_var.assign(try_match_first_node(haystack, input))
|
|
1090
|
+
.followed_by do
|
|
1091
|
+
haystack_var.assign(conversion.to_string(haystack_var))
|
|
1092
|
+
end
|
|
1093
|
+
.followed_by do
|
|
1094
|
+
start_var.assign(try_match_first_node(start, input))
|
|
1095
|
+
.followed_by do
|
|
1096
|
+
# Round the start position first (XPath 1.0 spec requires rounding)
|
|
1097
|
+
start_var.assign(conversion.to_float(start_var).round.to_i)
|
|
1098
|
+
end
|
|
1099
|
+
end
|
|
1100
|
+
.followed_by do
|
|
1101
|
+
if length
|
|
1102
|
+
length_var.assign(try_match_first_node(length, input))
|
|
1103
|
+
.followed_by do
|
|
1104
|
+
# Round the length (XPath 1.0 spec requires rounding)
|
|
1105
|
+
length_var.assign(conversion.to_float(length_var).round.to_i)
|
|
1106
|
+
end
|
|
1107
|
+
.followed_by do
|
|
1108
|
+
# XPath 1.0 algorithm:
|
|
1109
|
+
# If start < 1, some positions fall before the string
|
|
1110
|
+
# We need to adjust the effective length accordingly
|
|
1111
|
+
# effective_length = (start + length) - max(start, 1)
|
|
1112
|
+
# lua_start = max(start, 1) - 1 (since we start from position 1)
|
|
1113
|
+
|
|
1114
|
+
# Calculate how many positions to skip before position 1
|
|
1115
|
+
# If start is 0, we lose 1 position; if -2, we lose 3 positions
|
|
1116
|
+
ruby_start.assign(
|
|
1117
|
+
(start_var < literal(1))
|
|
1118
|
+
.if_true { literal(0) }
|
|
1119
|
+
.else { start_var - literal(1) },
|
|
1120
|
+
)
|
|
1121
|
+
end
|
|
1122
|
+
.followed_by do
|
|
1123
|
+
# Calculate effective length accounting for positions before string
|
|
1124
|
+
effective_length.assign(
|
|
1125
|
+
(start_var < literal(1))
|
|
1126
|
+
.if_true do
|
|
1127
|
+
# Some positions are before position 1
|
|
1128
|
+
# end_pos = start + length
|
|
1129
|
+
# effective = end_pos - 1 (since we start from position 1)
|
|
1130
|
+
# But clamp to 0 if entirely before string
|
|
1131
|
+
((start_var + length_var) - literal(1))
|
|
1132
|
+
.if_true { (start_var + length_var) - literal(1) }
|
|
1133
|
+
.else { literal(0) }
|
|
1134
|
+
end
|
|
1135
|
+
.else { length_var },
|
|
1136
|
+
)
|
|
1137
|
+
end
|
|
1138
|
+
.followed_by do
|
|
1139
|
+
# Clamp effective length to non-negative
|
|
1140
|
+
effective_length.assign(
|
|
1141
|
+
(effective_length < literal(0))
|
|
1142
|
+
.if_true { literal(0) }
|
|
1143
|
+
.else { effective_length },
|
|
1144
|
+
)
|
|
1145
|
+
end
|
|
1146
|
+
.followed_by do
|
|
1147
|
+
# Extract substring with effective length
|
|
1148
|
+
result_var.assign(haystack_var[ruby_start, effective_length])
|
|
1149
|
+
.followed_by do
|
|
1150
|
+
# Ensure we return empty string instead of nil
|
|
1151
|
+
result_var.assign(result_var.if_true do
|
|
1152
|
+
result_var
|
|
1153
|
+
end.else { string("") })
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
.followed_by do
|
|
1157
|
+
if block_given?
|
|
1158
|
+
result_var.empty?.if_false do
|
|
1159
|
+
yield result_var
|
|
1160
|
+
end
|
|
1161
|
+
else
|
|
1162
|
+
result_var
|
|
1163
|
+
end
|
|
1164
|
+
end
|
|
1165
|
+
else
|
|
1166
|
+
# No length specified - go to end of string
|
|
1167
|
+
# Convert to 0-based index, clamping to 0
|
|
1168
|
+
ruby_start.assign(
|
|
1169
|
+
(start_var < literal(1))
|
|
1170
|
+
.if_true { literal(0) }
|
|
1171
|
+
.else { start_var - literal(1) },
|
|
1172
|
+
).followed_by do
|
|
1173
|
+
# Extract from start to end
|
|
1174
|
+
result_var.assign(haystack_var[range(ruby_start, literal(-1))])
|
|
1175
|
+
.followed_by do
|
|
1176
|
+
# Ensure we return empty string instead of nil
|
|
1177
|
+
result_var.assign(result_var.if_true do
|
|
1178
|
+
result_var
|
|
1179
|
+
end.else { string("") })
|
|
1180
|
+
end
|
|
1181
|
+
end
|
|
1182
|
+
.followed_by do
|
|
1183
|
+
if block_given?
|
|
1184
|
+
result_var.empty?.if_false do
|
|
1185
|
+
yield result_var
|
|
1186
|
+
end
|
|
1187
|
+
else
|
|
1188
|
+
result_var
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
# 8. string-length() - Get string length
|
|
1196
|
+
def on_call_string_length(input, arg = nil)
|
|
1197
|
+
convert_var = unique_literal(:convert)
|
|
1198
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1199
|
+
|
|
1200
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1201
|
+
convert_var.assign(conversion.to_string(arg_var).length)
|
|
1202
|
+
.followed_by do
|
|
1203
|
+
if block_given?
|
|
1204
|
+
convert_var.zero?.if_false { yield convert_var }
|
|
1205
|
+
else
|
|
1206
|
+
convert_var.to_f
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
end
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
# 9. normalize-space() - Normalize whitespace
|
|
1213
|
+
def on_call_normalize_space(input, arg = nil)
|
|
1214
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1215
|
+
norm_var = unique_literal(:normalized)
|
|
1216
|
+
|
|
1217
|
+
# Create regex for matching whitespace sequences
|
|
1218
|
+
# Use Regexp.new to create /\s+/ pattern at runtime
|
|
1219
|
+
regexp_class = const_ref("Regexp")
|
|
1220
|
+
whitespace_pattern = string('\\s+')
|
|
1221
|
+
whitespace_regex = Ruby::Node.new(:send,
|
|
1222
|
+
[regexp_class, "new",
|
|
1223
|
+
whitespace_pattern])
|
|
1224
|
+
replace = string(" ")
|
|
1225
|
+
|
|
1226
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1227
|
+
norm_var
|
|
1228
|
+
.assign(conversion.to_string(arg_var).strip.gsub(whitespace_regex,
|
|
1229
|
+
replace))
|
|
1230
|
+
.followed_by do
|
|
1231
|
+
norm_var.empty?
|
|
1232
|
+
.if_true { string("") }
|
|
1233
|
+
.else { block_given? ? yield : norm_var }
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
# 10. translate() - Character replacement
|
|
1239
|
+
def on_call_translate(input, source, find, replace)
|
|
1240
|
+
source_var = unique_literal(:source)
|
|
1241
|
+
find_var = unique_literal(:find)
|
|
1242
|
+
replace_var = unique_literal(:replace)
|
|
1243
|
+
replaced_var = unique_literal(:replaced)
|
|
1244
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1245
|
+
|
|
1246
|
+
char = unique_literal(:char)
|
|
1247
|
+
index = unique_literal(:index)
|
|
1248
|
+
|
|
1249
|
+
source_var.assign(try_match_first_node(source, input))
|
|
1250
|
+
.followed_by do
|
|
1251
|
+
replaced_var.assign(conversion.to_string(source_var))
|
|
1252
|
+
end
|
|
1253
|
+
.followed_by do
|
|
1254
|
+
find_var.assign(try_match_first_node(find, input))
|
|
1255
|
+
end
|
|
1256
|
+
.followed_by do
|
|
1257
|
+
find_var.assign(conversion.to_string(find_var).chars.to_array)
|
|
1258
|
+
end
|
|
1259
|
+
.followed_by do
|
|
1260
|
+
replace_var.assign(try_match_first_node(replace, input))
|
|
1261
|
+
end
|
|
1262
|
+
.followed_by do
|
|
1263
|
+
replace_var.assign(conversion.to_string(replace_var).chars.to_array)
|
|
1264
|
+
end
|
|
1265
|
+
.followed_by do
|
|
1266
|
+
find_var.each_with_index.add_block(char, index) do
|
|
1267
|
+
replace_with = replace_var[index]
|
|
1268
|
+
.if_true { replace_var[index] }
|
|
1269
|
+
.else { string("") }
|
|
1270
|
+
|
|
1271
|
+
replaced_var.assign(replaced_var.gsub(char, replace_with))
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
.followed_by { replaced_var }
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
# ===== NUMERIC FUNCTIONS =====
|
|
1278
|
+
|
|
1279
|
+
# 1. number() - Convert to number
|
|
1280
|
+
def on_call_number(input, arg = nil, &block)
|
|
1281
|
+
convert_var = unique_literal(:convert)
|
|
1282
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1283
|
+
|
|
1284
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1285
|
+
convert_var.assign(conversion.to_float(arg_var)).followed_by do
|
|
1286
|
+
if block
|
|
1287
|
+
convert_var.zero?.if_false(&block)
|
|
1288
|
+
else
|
|
1289
|
+
convert_var
|
|
1290
|
+
end
|
|
1291
|
+
end
|
|
1292
|
+
end
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
# 2. sum() - Sum node values
|
|
1296
|
+
def on_call_sum(input, arg, &block)
|
|
1297
|
+
unless return_nodeset?(arg)
|
|
1298
|
+
raise TypeError, "sum() can only operate on a path, axis or predicate"
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
sum_var = unique_literal(:sum)
|
|
1302
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1303
|
+
|
|
1304
|
+
sum_var.assign(literal(0.0))
|
|
1305
|
+
.followed_by do
|
|
1306
|
+
process(arg, input) do |matched_node|
|
|
1307
|
+
sum_var.assign(sum_var + conversion.to_float(matched_node.text))
|
|
1308
|
+
end
|
|
1309
|
+
end
|
|
1310
|
+
.followed_by do
|
|
1311
|
+
block ? sum_var.zero?.if_false(&block) : sum_var
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# 3. count() - Count nodes
|
|
1316
|
+
def on_call_count(input, arg, &block)
|
|
1317
|
+
count = unique_literal(:count)
|
|
1318
|
+
|
|
1319
|
+
unless return_nodeset?(arg)
|
|
1320
|
+
raise TypeError, "count() can only operate on NodeSet instances"
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
count.assign(literal(0.0))
|
|
1324
|
+
.followed_by do
|
|
1325
|
+
process(arg, input) { count.assign(count + literal(1)) }
|
|
1326
|
+
end
|
|
1327
|
+
.followed_by do
|
|
1328
|
+
block ? count.zero?.if_false(&block) : count
|
|
1329
|
+
end
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
# 4. floor() - Round down
|
|
1333
|
+
def on_call_floor(input, arg)
|
|
1334
|
+
arg_ast = try_match_first_node(arg, input)
|
|
1335
|
+
call_arg = unique_literal(:call_arg)
|
|
1336
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1337
|
+
|
|
1338
|
+
call_arg.assign(arg_ast)
|
|
1339
|
+
.followed_by do
|
|
1340
|
+
call_arg.assign(conversion.to_float(call_arg))
|
|
1341
|
+
end
|
|
1342
|
+
.followed_by do
|
|
1343
|
+
call_arg.nan?
|
|
1344
|
+
.if_true { call_arg }
|
|
1345
|
+
.else { block_given? ? yield : call_arg.floor.to_f }
|
|
1346
|
+
end
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
# 5. ceiling() - Round up
|
|
1350
|
+
def on_call_ceiling(input, arg)
|
|
1351
|
+
arg_ast = try_match_first_node(arg, input)
|
|
1352
|
+
call_arg = unique_literal(:call_arg)
|
|
1353
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1354
|
+
|
|
1355
|
+
call_arg.assign(arg_ast)
|
|
1356
|
+
.followed_by do
|
|
1357
|
+
call_arg.assign(conversion.to_float(call_arg))
|
|
1358
|
+
end
|
|
1359
|
+
.followed_by do
|
|
1360
|
+
call_arg.nan?
|
|
1361
|
+
.if_true { call_arg }
|
|
1362
|
+
.else { block_given? ? yield : call_arg.ceil.to_f }
|
|
1363
|
+
end
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
# 6. round() - Round to nearest
|
|
1367
|
+
def on_call_round(input, arg)
|
|
1368
|
+
arg_ast = try_match_first_node(arg, input)
|
|
1369
|
+
call_arg = unique_literal(:call_arg)
|
|
1370
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1371
|
+
|
|
1372
|
+
call_arg.assign(arg_ast)
|
|
1373
|
+
.followed_by do
|
|
1374
|
+
call_arg.assign(conversion.to_float(call_arg))
|
|
1375
|
+
end
|
|
1376
|
+
.followed_by do
|
|
1377
|
+
call_arg.nan?
|
|
1378
|
+
.if_true { call_arg }
|
|
1379
|
+
.else { block_given? ? yield : call_arg.round.to_f }
|
|
1380
|
+
end
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
# ===== BOOLEAN FUNCTIONS =====
|
|
1384
|
+
|
|
1385
|
+
# 1. boolean() - Convert to boolean
|
|
1386
|
+
def on_call_boolean(input, arg, &block)
|
|
1387
|
+
arg_ast = try_match_first_node(arg, input)
|
|
1388
|
+
call_arg = unique_literal(:call_arg)
|
|
1389
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1390
|
+
|
|
1391
|
+
call_arg.assign(arg_ast).followed_by do
|
|
1392
|
+
converted = conversion.to_boolean(call_arg)
|
|
1393
|
+
|
|
1394
|
+
block ? converted.if_true(&block) : converted
|
|
1395
|
+
end
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
# 2. not() - Negate boolean
|
|
1399
|
+
def on_call_not(input, arg, &block)
|
|
1400
|
+
arg_ast = try_match_first_node(arg, input)
|
|
1401
|
+
call_arg = unique_literal(:call_arg)
|
|
1402
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1403
|
+
|
|
1404
|
+
call_arg.assign(arg_ast).followed_by do
|
|
1405
|
+
converted = conversion.to_boolean(call_arg).not
|
|
1406
|
+
|
|
1407
|
+
block ? converted.if_true(&block) : converted
|
|
1408
|
+
end
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
# 3. true() - Return true
|
|
1412
|
+
def on_call_true(*)
|
|
1413
|
+
block_given? ? yield : self_true
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
# 4. false() - Return false
|
|
1417
|
+
def on_call_false(*)
|
|
1418
|
+
self_false
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
# ===== NODE FUNCTIONS =====
|
|
1422
|
+
|
|
1423
|
+
# 1. local-name() - Get local name without namespace prefix
|
|
1424
|
+
def on_call_local_name(input, arg = nil)
|
|
1425
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1426
|
+
arg_var
|
|
1427
|
+
.if_true do
|
|
1428
|
+
ensure_element_or_attribute(arg_var)
|
|
1429
|
+
.followed_by { block_given? ? yield : arg_var.name }
|
|
1430
|
+
end
|
|
1431
|
+
.else { string("") }
|
|
1432
|
+
end
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
# 2. name() - Get expanded/qualified name with namespace
|
|
1436
|
+
def on_call_name(input, arg = nil)
|
|
1437
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1438
|
+
arg_var
|
|
1439
|
+
.if_true do
|
|
1440
|
+
ensure_element_or_attribute(arg_var)
|
|
1441
|
+
.followed_by { block_given? ? yield : arg_var.expanded_name }
|
|
1442
|
+
end
|
|
1443
|
+
.else { string("") }
|
|
1444
|
+
end
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
# 3. namespace-uri() - Get namespace URI
|
|
1448
|
+
def on_call_namespace_uri(input, arg = nil)
|
|
1449
|
+
default = string("")
|
|
1450
|
+
|
|
1451
|
+
argument_or_first_node(input, arg) do |arg_var|
|
|
1452
|
+
arg_var
|
|
1453
|
+
.if_true do
|
|
1454
|
+
ensure_element_or_attribute(arg_var).followed_by do
|
|
1455
|
+
arg_var.namespace
|
|
1456
|
+
.if_true { block_given? ? yield : arg_var.namespace.uri }
|
|
1457
|
+
.else { default }
|
|
1458
|
+
end
|
|
1459
|
+
end
|
|
1460
|
+
.else { default }
|
|
1461
|
+
end
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
# 4. lang() - Check xml:lang attribute
|
|
1465
|
+
def on_call_lang(input, arg)
|
|
1466
|
+
lang_var = unique_literal("lang")
|
|
1467
|
+
node = unique_literal("node")
|
|
1468
|
+
found = unique_literal("found")
|
|
1469
|
+
xml_lang = unique_literal("xml_lang")
|
|
1470
|
+
matched = unique_literal("matched")
|
|
1471
|
+
|
|
1472
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1473
|
+
|
|
1474
|
+
ast = lang_var.assign(try_match_first_node(arg, input))
|
|
1475
|
+
.followed_by do
|
|
1476
|
+
lang_var.assign(conversion.to_string(lang_var))
|
|
1477
|
+
end
|
|
1478
|
+
.followed_by do
|
|
1479
|
+
matched.assign(self_false)
|
|
1480
|
+
end
|
|
1481
|
+
.followed_by do
|
|
1482
|
+
node.assign(input)
|
|
1483
|
+
end
|
|
1484
|
+
.followed_by do
|
|
1485
|
+
xml_lang.assign(string("xml:lang"))
|
|
1486
|
+
end
|
|
1487
|
+
.followed_by do
|
|
1488
|
+
node.respond_to?(symbol(:attribute)).while_true do
|
|
1489
|
+
found.assign(node.get(xml_lang))
|
|
1490
|
+
.followed_by do
|
|
1491
|
+
found.if_true do
|
|
1492
|
+
found.eq(lang_var)
|
|
1493
|
+
.if_true do
|
|
1494
|
+
if block_given?
|
|
1495
|
+
yield
|
|
1496
|
+
else
|
|
1497
|
+
matched.assign(self_true).followed_by(break_loop)
|
|
1498
|
+
end
|
|
1499
|
+
end
|
|
1500
|
+
.else { break_loop }
|
|
1501
|
+
end
|
|
1502
|
+
end
|
|
1503
|
+
.followed_by(node.assign(node.parent))
|
|
1504
|
+
end
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
block_given? ? ast : ast.followed_by(matched)
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
# ===== POSITION FUNCTIONS =====
|
|
1511
|
+
|
|
1512
|
+
# 1. position() - Current position in predicate context
|
|
1513
|
+
def on_call_position(*)
|
|
1514
|
+
index = predicate_index
|
|
1515
|
+
|
|
1516
|
+
unless index
|
|
1517
|
+
raise InvalidContextError.new(
|
|
1518
|
+
"position() requires a predicate context. " \
|
|
1519
|
+
"Use position() within a predicate like: //item[position() = 1]",
|
|
1520
|
+
function_name: "position()",
|
|
1521
|
+
required_context: "predicate",
|
|
1522
|
+
)
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
index.to_f
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
# 2. last() - Size of current predicate context
|
|
1529
|
+
def on_call_last(*)
|
|
1530
|
+
set = predicate_nodeset
|
|
1531
|
+
|
|
1532
|
+
unless set
|
|
1533
|
+
raise InvalidContextError.new(
|
|
1534
|
+
"last() requires a predicate context. " \
|
|
1535
|
+
"Use last() within a predicate like: //item[position() = last()]",
|
|
1536
|
+
function_name: "last()",
|
|
1537
|
+
required_context: "predicate",
|
|
1538
|
+
)
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
set.length.to_f
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
# ===== SPECIAL FUNCTIONS =====
|
|
1545
|
+
|
|
1546
|
+
# 1. id() - Find nodes by ID attribute
|
|
1547
|
+
def on_call_id(input, arg)
|
|
1548
|
+
orig_input = original_input_literal
|
|
1549
|
+
node = unique_literal(:node)
|
|
1550
|
+
ids_var = unique_literal("ids")
|
|
1551
|
+
matched = unique_literal("id_matched")
|
|
1552
|
+
id_str_var = unique_literal("id_string")
|
|
1553
|
+
attr_var = unique_literal("attr")
|
|
1554
|
+
|
|
1555
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
1556
|
+
context_var = context_literal
|
|
1557
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
1558
|
+
|
|
1559
|
+
matched.assign(Ruby::Node.new(:send,
|
|
1560
|
+
[nodeset_class, "new", empty_array,
|
|
1561
|
+
context_var]))
|
|
1562
|
+
.followed_by do
|
|
1563
|
+
# When using a path, get text of all matched nodes
|
|
1564
|
+
if return_nodeset?(arg)
|
|
1565
|
+
empty_ids = Ruby::Node.new(:array, [])
|
|
1566
|
+
ids_var.assign(empty_ids).followed_by do
|
|
1567
|
+
process(arg, input) { |element| ids_var << element.text }
|
|
1568
|
+
end
|
|
1569
|
+
# Otherwise cast to string and split on spaces
|
|
1570
|
+
else
|
|
1571
|
+
conversion = literal(Moxml::XPath::Conversion)
|
|
1572
|
+
ids_var.assign(process(arg, input))
|
|
1573
|
+
.followed_by do
|
|
1574
|
+
ids_var.assign(conversion.to_string(ids_var).split(string(" ")))
|
|
1575
|
+
end
|
|
1576
|
+
end
|
|
1577
|
+
end
|
|
1578
|
+
.followed_by do
|
|
1579
|
+
id_str_var.assign(string("id"))
|
|
1580
|
+
end
|
|
1581
|
+
.followed_by do
|
|
1582
|
+
orig_input.each_node.add_block(node) do
|
|
1583
|
+
node.is_a?(const_ref("Moxml", "Element")).if_true do
|
|
1584
|
+
attr_var.assign(node.attribute(id_str_var)).followed_by do
|
|
1585
|
+
attr_var.and(ids_var.include?(attr_var.value))
|
|
1586
|
+
.if_true { block_given? ? yield : matched << node }
|
|
1587
|
+
end
|
|
1588
|
+
end
|
|
1589
|
+
end
|
|
1590
|
+
end
|
|
1591
|
+
.followed_by(matched)
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
# Helper methods
|
|
1595
|
+
|
|
1596
|
+
# Helper: Get argument or use current node's first child
|
|
1597
|
+
def argument_or_first_node(input, arg = nil)
|
|
1598
|
+
arg_ast = arg ? try_match_first_node(arg, input) : input
|
|
1599
|
+
arg_var = unique_literal(:argument_or_first_node)
|
|
1600
|
+
|
|
1601
|
+
arg_var.assign(arg_ast).followed_by { yield arg_var }
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
# Helper: Try to match first node v1
|
|
1605
|
+
def try_match_first_node_v1(ast, input, optimize_first = true)
|
|
1606
|
+
if return_nodeset?(ast) && optimize_first
|
|
1607
|
+
matched_set = unique_literal(:matched_set)
|
|
1608
|
+
first_node = unique_literal(:first_node)
|
|
1609
|
+
context_var = context_literal
|
|
1610
|
+
|
|
1611
|
+
# Create NodeSet for results
|
|
1612
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
1613
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
1614
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
1615
|
+
[nodeset_class, "new", empty_array,
|
|
1616
|
+
context_var])
|
|
1617
|
+
|
|
1618
|
+
matched_set.assign(nodeset_new)
|
|
1619
|
+
.followed_by do
|
|
1620
|
+
# Process with block to accumulate results
|
|
1621
|
+
process(ast, input) { |node| matched_set.push(node) }
|
|
1622
|
+
end
|
|
1623
|
+
.followed_by do
|
|
1624
|
+
first_node.assign(matched_set[literal(0)])
|
|
1625
|
+
end
|
|
1626
|
+
.followed_by do
|
|
1627
|
+
first_node.if_true { first_node }.else { string("") }
|
|
1628
|
+
end
|
|
1629
|
+
else
|
|
1630
|
+
process(ast, input)
|
|
1631
|
+
end
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1634
|
+
# Helper: Create mass assignment node
|
|
1635
|
+
def mass_assign(vars, value)
|
|
1636
|
+
Ruby::Node.new(:massign, [vars, value])
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
# Helper: Create range node for Ruby AST
|
|
1640
|
+
def range(start, stop)
|
|
1641
|
+
Ruby::Node.new(:range, [start, stop])
|
|
1642
|
+
end
|
|
1643
|
+
|
|
1644
|
+
# Helper: Ensure node is Element or Attribute
|
|
1645
|
+
def ensure_element_or_attribute(input)
|
|
1646
|
+
element_or_attribute(input).if_false do
|
|
1647
|
+
raise_message(TypeError, "argument is not an Element or Attribute")
|
|
1648
|
+
end
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# Helper: Raise an error with message
|
|
1652
|
+
def raise_message(klass, message)
|
|
1653
|
+
send_message(:raise, literal(klass), string(message))
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1656
|
+
# Helper: Send a message (for method calls like raise, break)
|
|
1657
|
+
def send_message(name, *args)
|
|
1658
|
+
Ruby::Node.new(:send, [nil, name.to_s] + args)
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
# Helper: Break statement
|
|
1662
|
+
def break_loop
|
|
1663
|
+
send_message(:break)
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
# Helper: Get current predicate index
|
|
1667
|
+
def predicate_index
|
|
1668
|
+
@predicate_indexes.last
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
# Helper: Get current predicate nodeset
|
|
1672
|
+
def predicate_nodeset
|
|
1673
|
+
@predicate_nodesets.last
|
|
1674
|
+
end
|
|
1675
|
+
|
|
1676
|
+
# Helper: Get original input literal for traversal
|
|
1677
|
+
def original_input_literal
|
|
1678
|
+
literal(:node)
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
# Helper: Generate code for an operator
|
|
1682
|
+
#
|
|
1683
|
+
# Processes left and right operands, optimizing to match only first node
|
|
1684
|
+
# when appropriate (path, axis, predicate)
|
|
1685
|
+
def operator(ast, input, optimize_first = true)
|
|
1686
|
+
left, right = ast.children
|
|
1687
|
+
|
|
1688
|
+
left_var = unique_literal(:op_left)
|
|
1689
|
+
right_var = unique_literal(:op_right)
|
|
1690
|
+
|
|
1691
|
+
left_ast = try_match_first_node(left, input, optimize_first)
|
|
1692
|
+
right_ast = try_match_first_node(right, input, optimize_first)
|
|
1693
|
+
|
|
1694
|
+
left_var.assign(left_ast)
|
|
1695
|
+
.followed_by(right_var.assign(right_ast))
|
|
1696
|
+
.followed_by { yield left_var, right_var }
|
|
1697
|
+
end
|
|
1698
|
+
|
|
1699
|
+
# Helper: Try to match first node in a set, otherwise process as usual
|
|
1700
|
+
def try_match_first_node(ast, input, optimize_first = true)
|
|
1701
|
+
if return_nodeset?(ast) && optimize_first
|
|
1702
|
+
matched_set = unique_literal(:matched_set)
|
|
1703
|
+
first_node = unique_literal(:first_node)
|
|
1704
|
+
context_var = context_literal
|
|
1705
|
+
|
|
1706
|
+
# Create NodeSet for results
|
|
1707
|
+
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
1708
|
+
empty_array = Ruby::Node.new(:array, [])
|
|
1709
|
+
nodeset_new = Ruby::Node.new(:send,
|
|
1710
|
+
[nodeset_class, "new", empty_array,
|
|
1711
|
+
context_var])
|
|
1712
|
+
|
|
1713
|
+
matched_set.assign(nodeset_new)
|
|
1714
|
+
.followed_by do
|
|
1715
|
+
# Process with block to accumulate results
|
|
1716
|
+
process(ast, input) { |node| matched_set.push(node) }
|
|
1717
|
+
end
|
|
1718
|
+
.followed_by do
|
|
1719
|
+
first_node.assign(matched_set[literal(0)])
|
|
1720
|
+
end
|
|
1721
|
+
.followed_by { first_node }
|
|
1722
|
+
else
|
|
1723
|
+
process(ast, input)
|
|
1724
|
+
end
|
|
1725
|
+
end
|
|
1726
|
+
|
|
1727
|
+
# Helper: Check if AST node is a number
|
|
1728
|
+
def number?(ast)
|
|
1729
|
+
%i[int float number].include?(ast.type)
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
# Helper: Check if AST contains a call node with given name
|
|
1733
|
+
def has_call_node?(ast, name)
|
|
1734
|
+
visit = [ast]
|
|
1735
|
+
|
|
1736
|
+
until visit.empty?
|
|
1737
|
+
current = visit.pop
|
|
1738
|
+
|
|
1739
|
+
return true if current.type == :call && current.children[0] == name
|
|
1740
|
+
|
|
1741
|
+
current.children.each do |child|
|
|
1742
|
+
visit << child if child.is_a?(AST::Node)
|
|
1743
|
+
end
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
false
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
# Helper: Catch a message (for early returns)
|
|
1750
|
+
def catch_message(name)
|
|
1751
|
+
send_message(:catch, symbol(name)).add_block do
|
|
1752
|
+
# Ensure catch only returns value when throw is invoked
|
|
1753
|
+
yield.followed_by(self_nil)
|
|
1754
|
+
end
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
# Helper: Throw a message with optional arguments
|
|
1758
|
+
def throw_message(name, *args)
|
|
1759
|
+
send_message(:throw, symbol(name), *args)
|
|
1760
|
+
end
|
|
1761
|
+
|
|
1762
|
+
# Helper: Variables literal for variable support
|
|
1763
|
+
def variables_literal
|
|
1764
|
+
literal(:variables)
|
|
1765
|
+
end
|
|
1766
|
+
end
|
|
1767
|
+
end
|
|
1768
|
+
end
|