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.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +5 -0
  3. data/.github/workflows/dependent-tests.yml +20 -0
  4. data/.github/workflows/docs.yml +59 -0
  5. data/.github/workflows/rake.yml +10 -10
  6. data/.github/workflows/release.yml +5 -3
  7. data/.gitignore +37 -0
  8. data/.rubocop.yml +15 -7
  9. data/.rubocop_todo.yml +238 -40
  10. data/Gemfile +14 -9
  11. data/LICENSE.md +6 -2
  12. data/README.adoc +535 -373
  13. data/Rakefile +53 -0
  14. data/benchmarks/.gitignore +6 -0
  15. data/benchmarks/generate_report.rb +550 -0
  16. data/docs/Gemfile +13 -0
  17. data/docs/_config.yml +138 -0
  18. data/docs/_guides/advanced-features.adoc +87 -0
  19. data/docs/_guides/development-testing.adoc +165 -0
  20. data/docs/_guides/index.adoc +45 -0
  21. data/docs/_guides/modifying-xml.adoc +293 -0
  22. data/docs/_guides/parsing-xml.adoc +231 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_pages/adapter-compatibility.adoc +369 -0
  26. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  27. data/docs/_pages/adapters/index.adoc +98 -0
  28. data/docs/_pages/adapters/libxml.adoc +286 -0
  29. data/docs/_pages/adapters/nokogiri.adoc +252 -0
  30. data/docs/_pages/adapters/oga.adoc +292 -0
  31. data/docs/_pages/adapters/ox.adoc +55 -0
  32. data/docs/_pages/adapters/rexml.adoc +293 -0
  33. data/docs/_pages/best-practices.adoc +430 -0
  34. data/docs/_pages/compatibility.adoc +468 -0
  35. data/docs/_pages/configuration.adoc +251 -0
  36. data/docs/_pages/error-handling.adoc +350 -0
  37. data/docs/_pages/headed-ox-limitations.adoc +558 -0
  38. data/docs/_pages/headed-ox.adoc +1025 -0
  39. data/docs/_pages/index.adoc +35 -0
  40. data/docs/_pages/installation.adoc +141 -0
  41. data/docs/_pages/node-api-reference.adoc +50 -0
  42. data/docs/_pages/performance.adoc +36 -0
  43. data/docs/_pages/quick-start.adoc +244 -0
  44. data/docs/_pages/thread-safety.adoc +29 -0
  45. data/docs/_references/document-api.adoc +408 -0
  46. data/docs/_references/index.adoc +48 -0
  47. data/docs/_tutorials/basic-usage.adoc +268 -0
  48. data/docs/_tutorials/builder-pattern.adoc +343 -0
  49. data/docs/_tutorials/index.adoc +33 -0
  50. data/docs/_tutorials/namespace-handling.adoc +325 -0
  51. data/docs/_tutorials/xpath-queries.adoc +359 -0
  52. data/docs/index.adoc +122 -0
  53. data/examples/README.md +124 -0
  54. data/examples/api_client/README.md +424 -0
  55. data/examples/api_client/api_client.rb +394 -0
  56. data/examples/api_client/example_response.xml +48 -0
  57. data/examples/headed_ox_example/README.md +90 -0
  58. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  59. data/examples/rss_parser/README.md +194 -0
  60. data/examples/rss_parser/example_feed.xml +93 -0
  61. data/examples/rss_parser/rss_parser.rb +189 -0
  62. data/examples/sax_parsing/README.md +50 -0
  63. data/examples/sax_parsing/data_extractor.rb +75 -0
  64. data/examples/sax_parsing/example.xml +21 -0
  65. data/examples/sax_parsing/large_file.rb +78 -0
  66. data/examples/sax_parsing/simple_parser.rb +55 -0
  67. data/examples/web_scraper/README.md +352 -0
  68. data/examples/web_scraper/example_page.html +201 -0
  69. data/examples/web_scraper/web_scraper.rb +312 -0
  70. data/lib/moxml/adapter/base.rb +107 -28
  71. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  72. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  73. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  74. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  75. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  76. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  77. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  78. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  79. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
  80. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
  81. data/lib/moxml/adapter/headed_ox.rb +161 -0
  82. data/lib/moxml/adapter/libxml.rb +1548 -0
  83. data/lib/moxml/adapter/nokogiri.rb +121 -9
  84. data/lib/moxml/adapter/oga.rb +123 -12
  85. data/lib/moxml/adapter/ox.rb +282 -26
  86. data/lib/moxml/adapter/rexml.rb +127 -20
  87. data/lib/moxml/adapter.rb +21 -4
  88. data/lib/moxml/attribute.rb +6 -0
  89. data/lib/moxml/builder.rb +40 -4
  90. data/lib/moxml/config.rb +8 -3
  91. data/lib/moxml/context.rb +39 -1
  92. data/lib/moxml/doctype.rb +13 -1
  93. data/lib/moxml/document.rb +39 -6
  94. data/lib/moxml/document_builder.rb +27 -5
  95. data/lib/moxml/element.rb +71 -2
  96. data/lib/moxml/error.rb +175 -6
  97. data/lib/moxml/node.rb +94 -3
  98. data/lib/moxml/node_set.rb +34 -0
  99. data/lib/moxml/sax/block_handler.rb +194 -0
  100. data/lib/moxml/sax/element_handler.rb +124 -0
  101. data/lib/moxml/sax/handler.rb +113 -0
  102. data/lib/moxml/sax.rb +31 -0
  103. data/lib/moxml/version.rb +1 -1
  104. data/lib/moxml/xml_utils/encoder.rb +4 -4
  105. data/lib/moxml/xml_utils.rb +7 -4
  106. data/lib/moxml/xpath/ast/node.rb +159 -0
  107. data/lib/moxml/xpath/cache.rb +91 -0
  108. data/lib/moxml/xpath/compiler.rb +1768 -0
  109. data/lib/moxml/xpath/context.rb +26 -0
  110. data/lib/moxml/xpath/conversion.rb +124 -0
  111. data/lib/moxml/xpath/engine.rb +52 -0
  112. data/lib/moxml/xpath/errors.rb +101 -0
  113. data/lib/moxml/xpath/lexer.rb +304 -0
  114. data/lib/moxml/xpath/parser.rb +485 -0
  115. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  116. data/lib/moxml/xpath/ruby/node.rb +193 -0
  117. data/lib/moxml/xpath.rb +37 -0
  118. data/lib/moxml.rb +5 -2
  119. data/moxml.gemspec +3 -1
  120. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  121. data/spec/consistency/README.md +77 -0
  122. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  123. data/spec/examples/README.md +75 -0
  124. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  125. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  126. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  127. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  128. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  129. data/spec/integration/README.md +71 -0
  130. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  131. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  132. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  133. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  134. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  135. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  136. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  137. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  138. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  139. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
  140. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  141. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  142. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  143. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  144. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  145. data/spec/moxml/README.md +41 -0
  146. data/spec/moxml/adapter/.gitkeep +0 -0
  147. data/spec/moxml/adapter/README.md +61 -0
  148. data/spec/moxml/adapter/base_spec.rb +27 -0
  149. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  150. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  151. data/spec/moxml/adapter/ox_spec.rb +9 -8
  152. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  153. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  154. data/spec/moxml/adapter_spec.rb +16 -0
  155. data/spec/moxml/attribute_spec.rb +30 -0
  156. data/spec/moxml/builder_spec.rb +33 -0
  157. data/spec/moxml/cdata_spec.rb +31 -0
  158. data/spec/moxml/comment_spec.rb +31 -0
  159. data/spec/moxml/config_spec.rb +3 -3
  160. data/spec/moxml/context_spec.rb +28 -0
  161. data/spec/moxml/declaration_spec.rb +36 -0
  162. data/spec/moxml/doctype_spec.rb +33 -0
  163. data/spec/moxml/document_builder_spec.rb +30 -0
  164. data/spec/moxml/document_spec.rb +105 -0
  165. data/spec/moxml/element_spec.rb +143 -0
  166. data/spec/moxml/error_spec.rb +266 -22
  167. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  168. data/spec/moxml/namespace_spec.rb +32 -0
  169. data/spec/moxml/node_set_spec.rb +39 -0
  170. data/spec/moxml/node_spec.rb +37 -0
  171. data/spec/moxml/processing_instruction_spec.rb +34 -0
  172. data/spec/moxml/sax_spec.rb +1067 -0
  173. data/spec/moxml/text_spec.rb +31 -0
  174. data/spec/moxml/version_spec.rb +14 -0
  175. data/spec/moxml/xml_utils/.gitkeep +0 -0
  176. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  177. data/spec/moxml/xml_utils_spec.rb +49 -0
  178. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  179. data/spec/moxml/xpath/axes_spec.rb +296 -0
  180. data/spec/moxml/xpath/cache_spec.rb +358 -0
  181. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  182. data/spec/moxml/xpath/context_spec.rb +210 -0
  183. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  184. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  185. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  186. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  187. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  188. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  189. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  190. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  191. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  192. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  193. data/spec/moxml/xpath/parser_spec.rb +364 -0
  194. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  195. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  196. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  197. data/spec/moxml/xpath_spec.rb +77 -0
  198. data/spec/performance/README.md +83 -0
  199. data/spec/performance/benchmark_spec.rb +64 -0
  200. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
  201. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  202. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  203. data/spec/spec_helper.rb +58 -1
  204. data/spec/support/xml_matchers.rb +1 -1
  205. metadata +176 -34
  206. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  207. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  208. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  209. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  210. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Parser do
6
+ describe ".parse" do
7
+ context "simple paths" do
8
+ it "parses root element" do
9
+ ast = described_class.parse("/root")
10
+ expect(ast.type).to eq(:absolute_path)
11
+ expect(ast.children).not_to be_empty
12
+ end
13
+
14
+ it "parses simple child path" do
15
+ ast = described_class.parse("/root/child")
16
+ expect(ast.type).to eq(:absolute_path)
17
+ end
18
+
19
+ it "parses descendant path" do
20
+ ast = described_class.parse("//book")
21
+ expect(ast.type).to eq(:absolute_path)
22
+ end
23
+
24
+ it "parses relative path" do
25
+ ast = described_class.parse("book/title")
26
+ expect(ast.type).to eq(:relative_path)
27
+ end
28
+
29
+ it "parses single element" do
30
+ ast = described_class.parse("book")
31
+ expect(ast.type).to eq(:relative_path)
32
+ end
33
+
34
+ it "parses wildcard" do
35
+ ast = described_class.parse("*")
36
+ expect(ast.type).to eq(:relative_path)
37
+ end
38
+
39
+ it "parses descendant wildcard" do
40
+ ast = described_class.parse("//*")
41
+ expect(ast.type).to eq(:absolute_path)
42
+ end
43
+ end
44
+
45
+ context "abbreviated steps" do
46
+ it "parses current node (.)" do
47
+ ast = described_class.parse(".")
48
+ expect(ast.type).to eq(:relative_path)
49
+ expect(ast.children.first.type).to eq(:current)
50
+ end
51
+
52
+ it "parses parent node (..)" do
53
+ ast = described_class.parse("..")
54
+ expect(ast.type).to eq(:relative_path)
55
+ expect(ast.children.first.type).to eq(:parent)
56
+ end
57
+
58
+ it "parses attribute" do
59
+ ast = described_class.parse("@id")
60
+ expect(ast.type).to eq(:relative_path)
61
+ end
62
+
63
+ it "parses path with parent reference" do
64
+ ast = described_class.parse("../book")
65
+ expect(ast.type).to eq(:relative_path)
66
+ end
67
+ end
68
+
69
+ context "axis specifications" do
70
+ it "parses child axis" do
71
+ ast = described_class.parse("child::book")
72
+ expect(ast.type).to eq(:relative_path)
73
+ end
74
+
75
+ it "parses descendant axis" do
76
+ ast = described_class.parse("descendant::book")
77
+ expect(ast.type).to eq(:relative_path)
78
+ end
79
+
80
+ it "parses attribute axis" do
81
+ ast = described_class.parse("attribute::id")
82
+ expect(ast.type).to eq(:relative_path)
83
+ end
84
+
85
+ it "parses parent axis" do
86
+ ast = described_class.parse("parent::section")
87
+ expect(ast.type).to eq(:relative_path)
88
+ end
89
+
90
+ it "parses following-sibling axis" do
91
+ ast = described_class.parse("following-sibling::chapter")
92
+ expect(ast.type).to eq(:relative_path)
93
+ end
94
+
95
+ it "parses ancestor-or-self axis" do
96
+ ast = described_class.parse("ancestor-or-self::div")
97
+ expect(ast.type).to eq(:relative_path)
98
+ end
99
+ end
100
+
101
+ context "node tests" do
102
+ it "parses text() node test" do
103
+ ast = described_class.parse("text()")
104
+ expect(ast.type).to eq(:relative_path)
105
+ end
106
+
107
+ it "parses comment() node test" do
108
+ ast = described_class.parse("comment()")
109
+ expect(ast.type).to eq(:relative_path)
110
+ end
111
+
112
+ it "parses node() node test" do
113
+ ast = described_class.parse("node()")
114
+ expect(ast.type).to eq(:relative_path)
115
+ end
116
+
117
+ it "parses processing-instruction() node test" do
118
+ ast = described_class.parse("processing-instruction()")
119
+ expect(ast.type).to eq(:relative_path)
120
+ end
121
+ end
122
+
123
+ context "predicates" do
124
+ it "parses simple attribute predicate" do
125
+ ast = described_class.parse("book[@id]")
126
+ expect(ast.type).to eq(:relative_path)
127
+ end
128
+
129
+ it "parses comparison predicate" do
130
+ ast = described_class.parse("book[@price < 10]")
131
+ expect(ast.type).to eq(:relative_path)
132
+ end
133
+
134
+ it "parses position predicate" do
135
+ ast = described_class.parse("book[1]")
136
+ expect(ast.type).to eq(:relative_path)
137
+ end
138
+
139
+ it "parses multiple predicates" do
140
+ ast = described_class.parse("book[@id][@lang]")
141
+ expect(ast.type).to eq(:relative_path)
142
+ end
143
+
144
+ it "parses complex predicate" do
145
+ ast = described_class.parse("book[@price < 10 and @year > 2000]")
146
+ expect(ast.type).to eq(:relative_path)
147
+ end
148
+
149
+ it "parses nested predicates" do
150
+ ast = described_class.parse('//book[author[@country="USA"]]')
151
+ expect(ast.type).to eq(:absolute_path)
152
+ end
153
+ end
154
+
155
+ context "operators" do
156
+ it "parses all comparison operators without error" do
157
+ %w[= != < > <= >=].each do |op|
158
+ expect { described_class.parse("@a #{op} @b") }.not_to raise_error
159
+ end
160
+ end
161
+
162
+ it "parses all arithmetic operators without error" do
163
+ operators = { "+" => true, "-" => true, "*" => true, "div" => true,
164
+ "mod" => true }
165
+ operators.each_key do |op|
166
+ expect { described_class.parse("@a #{op} @b") }.not_to raise_error
167
+ end
168
+ end
169
+ end
170
+
171
+ context "logical operators" do
172
+ it "parses logical operators without error" do
173
+ expect { described_class.parse("@a and @b") }.not_to raise_error
174
+ expect { described_class.parse("@a or @b") }.not_to raise_error
175
+ expect { described_class.parse("@a and @b or @c") }.not_to raise_error
176
+ end
177
+ end
178
+
179
+ context "literals" do
180
+ it "parses string literal" do
181
+ ast = described_class.parse('"hello"')
182
+ expect(ast.type).to eq(:string)
183
+ expect(ast.value).to eq("hello")
184
+ end
185
+
186
+ it "parses single-quoted string" do
187
+ ast = described_class.parse("'world'")
188
+ expect(ast.type).to eq(:string)
189
+ expect(ast.value).to eq("world")
190
+ end
191
+
192
+ it "parses number literal" do
193
+ ast = described_class.parse("123")
194
+ expect(ast.type).to eq(:number)
195
+ expect(ast.value).to eq(123)
196
+ end
197
+
198
+ it "parses decimal literal" do
199
+ ast = described_class.parse("123.45")
200
+ expect(ast.type).to eq(:number)
201
+ expect(ast.value).to eq(123.45)
202
+ end
203
+ end
204
+
205
+ context "function calls" do
206
+ it "parses functions with varying argument counts" do
207
+ expect { described_class.parse("position()") }.not_to raise_error
208
+ expect { described_class.parse("count(//item)") }.not_to raise_error
209
+ expect do
210
+ described_class.parse("substring(@name, 1, 3)")
211
+ end.not_to raise_error
212
+ expect do
213
+ described_class.parse("sum(count(//item))")
214
+ end.not_to raise_error
215
+ end
216
+ end
217
+
218
+ context "union expressions" do
219
+ it "parses union operators without error" do
220
+ expect { described_class.parse("book | article") }.not_to raise_error
221
+ expect do
222
+ described_class.parse("book | article | chapter")
223
+ end.not_to raise_error
224
+ expect do
225
+ described_class.parse("//book | //article")
226
+ end.not_to raise_error
227
+ end
228
+ end
229
+
230
+ context "variables" do
231
+ it "parses variable references without error" do
232
+ expect { described_class.parse("$var") }.not_to raise_error
233
+ expect { described_class.parse("$price * 1.1") }.not_to raise_error
234
+ end
235
+ end
236
+
237
+ context "namespaces" do
238
+ it "parses namespaced element" do
239
+ ast = described_class.parse("ns:element")
240
+ expect(ast.type).to eq(:relative_path)
241
+ end
242
+
243
+ it "parses namespaced path" do
244
+ ast = described_class.parse("/ns:root/ns:child")
245
+ expect(ast.type).to eq(:absolute_path)
246
+ end
247
+
248
+ it "parses namespace wildcard" do
249
+ ast = described_class.parse("ns:*")
250
+ expect(ast.type).to eq(:relative_path)
251
+ end
252
+ end
253
+
254
+ context "complex expressions" do
255
+ it "parses complex predicate with paths" do
256
+ ast = described_class.parse("//book[@price < 10]/title")
257
+ expect(ast.type).to eq(:absolute_path)
258
+ end
259
+
260
+ it "parses nested paths in predicates" do
261
+ ast = described_class.parse('//book[author/name="Smith"]')
262
+ expect(ast.type).to eq(:absolute_path)
263
+ end
264
+
265
+ it "parses arithmetic in predicates" do
266
+ ast = described_class.parse("//book[@price * 1.1 < 100]")
267
+ expect(ast.type).to eq(:absolute_path)
268
+ end
269
+
270
+ it "parses function calls in predicates" do
271
+ ast = described_class.parse("//book[position() = 1]")
272
+ expect(ast.type).to eq(:absolute_path)
273
+ end
274
+
275
+ it "parses grouped expressions" do
276
+ ast = described_class.parse("(@a + @b) * @c")
277
+ expect(ast).to be_a(Moxml::XPath::AST::Node)
278
+ # Grouped expressions parse successfully
279
+ end
280
+ end
281
+
282
+ context "operator precedence" do
283
+ it "parses arithmetic precedence correctly" do
284
+ ast = described_class.parse("1 + 2 * 3")
285
+ expect(ast).to be_a(Moxml::XPath::AST::Node)
286
+ # Test actual execution would give 7 (not 9), proving correct precedence
287
+ end
288
+
289
+ it "parses comparison precedence correctly" do
290
+ ast = described_class.parse("1 + 2 < 5")
291
+ expect(ast).to be_a(Moxml::XPath::AST::Node)
292
+ # Precedence: (1 + 2) < 5, not 1 + (2 < 5)
293
+ end
294
+
295
+ it "parses logical precedence correctly" do
296
+ ast = described_class.parse("true and false or true")
297
+ expect(ast).to be_a(Moxml::XPath::AST::Node)
298
+ # Precedence: (true and false) or true, not true and (false or true)
299
+ end
300
+ end
301
+
302
+ context "edge cases" do
303
+ it "parses empty expression as empty node" do
304
+ ast = described_class.parse("")
305
+ expect(ast.type).to eq(:empty)
306
+ end
307
+
308
+ it "parses slash-only path" do
309
+ ast = described_class.parse("/")
310
+ expect(ast.type).to eq(:absolute_path)
311
+ end
312
+
313
+ it "parses whitespace-only expression as empty" do
314
+ ast = described_class.parse(" ")
315
+ expect(ast.type).to eq(:empty)
316
+ end
317
+ end
318
+
319
+ context "error handling" do
320
+ it "raises error for unexpected token" do
321
+ expect { described_class.parse("book]") }
322
+ .to raise_error(Moxml::XPath::SyntaxError)
323
+ end
324
+
325
+ it "raises error for unclosed bracket" do
326
+ expect { described_class.parse("book[1") }
327
+ .to raise_error(Moxml::XPath::SyntaxError, /Expected '\]'/)
328
+ end
329
+
330
+ it "raises error for unclosed parenthesis" do
331
+ expect { described_class.parse("count(//item") }
332
+ .to raise_error(Moxml::XPath::SyntaxError, /Expected '\)'/)
333
+ end
334
+
335
+ it "raises error for missing node test" do
336
+ expect { described_class.parse("child::") }
337
+ .to raise_error(Moxml::XPath::SyntaxError, /Expected node test/)
338
+ end
339
+
340
+ it "raises error for invalid axis" do
341
+ expect { described_class.parse("invalid::book") }
342
+ .to raise_error(Moxml::XPath::SyntaxError)
343
+ end
344
+ end
345
+ end
346
+
347
+ describe ".parse_with_cache" do
348
+ it "caches parsed expressions" do
349
+ expr = '//book[@id="123"]'
350
+ ast1 = described_class.parse_with_cache(expr)
351
+ ast2 = described_class.parse_with_cache(expr)
352
+
353
+ # Should return same cached object
354
+ expect(ast1).to be(ast2)
355
+ end
356
+
357
+ it "handles different expressions" do
358
+ ast1 = described_class.parse_with_cache("//book")
359
+ ast2 = described_class.parse_with_cache("//article")
360
+
361
+ expect(ast1).not_to be(ast2)
362
+ end
363
+ end
364
+ end