moxml 0.1.7 → 0.1.9

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 (215) 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 +224 -43
  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 +51 -0
  21. data/docs/_guides/modifying-xml.adoc +292 -0
  22. data/docs/_guides/parsing-xml.adoc +230 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_guides/xml-declaration.adoc +450 -0
  26. data/docs/_pages/adapter-compatibility.adoc +369 -0
  27. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  28. data/docs/_pages/adapters/index.adoc +97 -0
  29. data/docs/_pages/adapters/libxml.adoc +285 -0
  30. data/docs/_pages/adapters/nokogiri.adoc +251 -0
  31. data/docs/_pages/adapters/oga.adoc +291 -0
  32. data/docs/_pages/adapters/ox.adoc +56 -0
  33. data/docs/_pages/adapters/rexml.adoc +292 -0
  34. data/docs/_pages/best-practices.adoc +429 -0
  35. data/docs/_pages/compatibility.adoc +467 -0
  36. data/docs/_pages/configuration.adoc +250 -0
  37. data/docs/_pages/error-handling.adoc +349 -0
  38. data/docs/_pages/headed-ox-limitations.adoc +574 -0
  39. data/docs/_pages/headed-ox.adoc +1025 -0
  40. data/docs/_pages/index.adoc +35 -0
  41. data/docs/_pages/installation.adoc +140 -0
  42. data/docs/_pages/node-api-reference.adoc +49 -0
  43. data/docs/_pages/performance.adoc +35 -0
  44. data/docs/_pages/quick-start.adoc +243 -0
  45. data/docs/_pages/thread-safety.adoc +28 -0
  46. data/docs/_references/document-api.adoc +407 -0
  47. data/docs/_references/index.adoc +48 -0
  48. data/docs/_tutorials/basic-usage.adoc +267 -0
  49. data/docs/_tutorials/builder-pattern.adoc +342 -0
  50. data/docs/_tutorials/index.adoc +33 -0
  51. data/docs/_tutorials/namespace-handling.adoc +324 -0
  52. data/docs/_tutorials/xpath-queries.adoc +358 -0
  53. data/docs/index.adoc +122 -0
  54. data/examples/README.md +124 -0
  55. data/examples/api_client/README.md +424 -0
  56. data/examples/api_client/api_client.rb +394 -0
  57. data/examples/api_client/example_response.xml +48 -0
  58. data/examples/headed_ox_example/README.md +90 -0
  59. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  60. data/examples/rss_parser/README.md +194 -0
  61. data/examples/rss_parser/example_feed.xml +93 -0
  62. data/examples/rss_parser/rss_parser.rb +189 -0
  63. data/examples/sax_parsing/README.md +50 -0
  64. data/examples/sax_parsing/data_extractor.rb +75 -0
  65. data/examples/sax_parsing/example.xml +21 -0
  66. data/examples/sax_parsing/large_file.rb +78 -0
  67. data/examples/sax_parsing/simple_parser.rb +55 -0
  68. data/examples/web_scraper/README.md +352 -0
  69. data/examples/web_scraper/example_page.html +201 -0
  70. data/examples/web_scraper/web_scraper.rb +312 -0
  71. data/lib/moxml/adapter/base.rb +107 -28
  72. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  73. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  74. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  75. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  76. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  77. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  78. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  79. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  80. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
  81. data/lib/moxml/adapter/customized_rexml/formatter.rb +13 -8
  82. data/lib/moxml/adapter/headed_ox.rb +161 -0
  83. data/lib/moxml/adapter/libxml.rb +1564 -0
  84. data/lib/moxml/adapter/nokogiri.rb +156 -9
  85. data/lib/moxml/adapter/oga.rb +190 -15
  86. data/lib/moxml/adapter/ox.rb +322 -28
  87. data/lib/moxml/adapter/rexml.rb +157 -28
  88. data/lib/moxml/adapter.rb +21 -4
  89. data/lib/moxml/attribute.rb +6 -0
  90. data/lib/moxml/builder.rb +40 -4
  91. data/lib/moxml/config.rb +8 -3
  92. data/lib/moxml/context.rb +57 -2
  93. data/lib/moxml/declaration.rb +9 -0
  94. data/lib/moxml/doctype.rb +13 -1
  95. data/lib/moxml/document.rb +53 -6
  96. data/lib/moxml/document_builder.rb +34 -5
  97. data/lib/moxml/element.rb +71 -2
  98. data/lib/moxml/error.rb +175 -6
  99. data/lib/moxml/node.rb +155 -4
  100. data/lib/moxml/node_set.rb +34 -0
  101. data/lib/moxml/sax/block_handler.rb +194 -0
  102. data/lib/moxml/sax/element_handler.rb +124 -0
  103. data/lib/moxml/sax/handler.rb +113 -0
  104. data/lib/moxml/sax.rb +31 -0
  105. data/lib/moxml/version.rb +1 -1
  106. data/lib/moxml/xml_utils/encoder.rb +4 -4
  107. data/lib/moxml/xml_utils.rb +7 -4
  108. data/lib/moxml/xpath/ast/node.rb +159 -0
  109. data/lib/moxml/xpath/cache.rb +91 -0
  110. data/lib/moxml/xpath/compiler.rb +1770 -0
  111. data/lib/moxml/xpath/context.rb +26 -0
  112. data/lib/moxml/xpath/conversion.rb +124 -0
  113. data/lib/moxml/xpath/engine.rb +52 -0
  114. data/lib/moxml/xpath/errors.rb +101 -0
  115. data/lib/moxml/xpath/lexer.rb +304 -0
  116. data/lib/moxml/xpath/parser.rb +485 -0
  117. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  118. data/lib/moxml/xpath/ruby/node.rb +193 -0
  119. data/lib/moxml/xpath.rb +37 -0
  120. data/lib/moxml.rb +5 -2
  121. data/moxml.gemspec +3 -1
  122. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  123. data/spec/consistency/README.md +77 -0
  124. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  125. data/spec/examples/README.md +75 -0
  126. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  127. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  128. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  129. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  130. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  131. data/spec/integration/README.md +71 -0
  132. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  133. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  134. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  135. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  136. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  137. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  138. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  139. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  140. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  141. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -5
  142. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  143. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  144. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  145. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  146. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  147. data/spec/moxml/README.md +41 -0
  148. data/spec/moxml/adapter/.gitkeep +0 -0
  149. data/spec/moxml/adapter/README.md +61 -0
  150. data/spec/moxml/adapter/base_spec.rb +27 -0
  151. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  152. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  153. data/spec/moxml/adapter/ox_spec.rb +9 -8
  154. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  155. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  156. data/spec/moxml/adapter_spec.rb +16 -0
  157. data/spec/moxml/attribute_spec.rb +30 -0
  158. data/spec/moxml/builder_spec.rb +33 -0
  159. data/spec/moxml/cdata_spec.rb +31 -0
  160. data/spec/moxml/comment_spec.rb +31 -0
  161. data/spec/moxml/config_spec.rb +3 -3
  162. data/spec/moxml/context_spec.rb +28 -0
  163. data/spec/moxml/declaration_preservation_spec.rb +217 -0
  164. data/spec/moxml/declaration_spec.rb +36 -0
  165. data/spec/moxml/doctype_spec.rb +33 -0
  166. data/spec/moxml/document_builder_spec.rb +30 -0
  167. data/spec/moxml/document_spec.rb +105 -0
  168. data/spec/moxml/element_spec.rb +143 -0
  169. data/spec/moxml/error_spec.rb +266 -22
  170. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  171. data/spec/moxml/namespace_spec.rb +32 -0
  172. data/spec/moxml/node_set_spec.rb +39 -0
  173. data/spec/moxml/node_spec.rb +37 -0
  174. data/spec/moxml/processing_instruction_spec.rb +34 -0
  175. data/spec/moxml/sax_spec.rb +1067 -0
  176. data/spec/moxml/text_spec.rb +31 -0
  177. data/spec/moxml/version_spec.rb +14 -0
  178. data/spec/moxml/xml_utils/.gitkeep +0 -0
  179. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  180. data/spec/moxml/xml_utils_spec.rb +49 -0
  181. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  182. data/spec/moxml/xpath/axes_spec.rb +296 -0
  183. data/spec/moxml/xpath/cache_spec.rb +358 -0
  184. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  185. data/spec/moxml/xpath/context_spec.rb +210 -0
  186. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  187. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  188. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  189. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  190. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  191. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  192. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  193. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  194. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  195. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  196. data/spec/moxml/xpath/parser_spec.rb +364 -0
  197. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  198. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  199. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  200. data/spec/moxml/xpath_spec.rb +77 -0
  201. data/spec/performance/README.md +83 -0
  202. data/spec/performance/benchmark_spec.rb +64 -0
  203. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +4 -1
  204. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  205. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  206. data/spec/spec_helper.rb +58 -1
  207. data/spec/support/xml_matchers.rb +1 -1
  208. metadata +178 -34
  209. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  210. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  213. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  214. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  215. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Text do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root>plain text</root>") }
8
+
9
+ describe "#content" do
10
+ it "returns text content" do
11
+ text = doc.root.children.first
12
+ expect(text).to be_a(described_class)
13
+ expect(text.content).to eq("plain text")
14
+ end
15
+ end
16
+
17
+ describe "#to_xml" do
18
+ it "returns XML representation" do
19
+ text = doc.root.children.first
20
+ expect(text.to_xml).to eq("plain text")
21
+ end
22
+ end
23
+
24
+ describe "creation" do
25
+ it "creates a text node" do
26
+ text = doc.create_text("new text")
27
+ expect(text).to be_a(described_class)
28
+ expect(text.content).to eq("new text")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Moxml::VERSION" do
6
+ it "has a version number" do
7
+ expect(Moxml::VERSION).not_to be_nil
8
+ expect(Moxml::VERSION).to be_a(String)
9
+ end
10
+
11
+ it "follows semantic versioning" do
12
+ expect(Moxml::VERSION).to match(/\d+\.\d+\.\d+/)
13
+ end
14
+ end
File without changes
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XmlUtils::Encoder do
6
+ describe "#call (encoding)" do
7
+ it "encodes XML special characters in basic mode" do
8
+ encoder = described_class.new("<tag>content & more</tag>", :basic)
9
+ result = encoder.call
10
+ expect(result).to include("&lt;")
11
+ expect(result).to include("&gt;")
12
+ expect(result).to include("&amp;")
13
+ end
14
+
15
+ it "encodes quotes in full mode" do
16
+ encoder = described_class.new('"value"', :full)
17
+ result = encoder.call
18
+ expect(result).to include("&quot;")
19
+ end
20
+
21
+ it "doesn't encode in none mode" do
22
+ encoder = described_class.new("<tag>", :none)
23
+ result = encoder.call
24
+ expect(result).to eq("<tag>")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XmlUtils do
6
+ # XmlUtils is a mixin module, create a test class that includes it
7
+ let(:test_class) do
8
+ Class.new do
9
+ include Moxml::XmlUtils
10
+ end
11
+ end
12
+ let(:utils) { test_class.new }
13
+
14
+ describe "#validate_declaration_version" do
15
+ it "validates XML versions" do
16
+ expect { utils.validate_declaration_version("1.0") }.not_to raise_error
17
+ expect { utils.validate_declaration_version("1.1") }.not_to raise_error
18
+ end
19
+
20
+ it "raises error for invalid version" do
21
+ expect do
22
+ utils.validate_declaration_version("2.0")
23
+ end.to raise_error(Moxml::ValidationError, "Invalid XML version: 2.0")
24
+ end
25
+ end
26
+
27
+ describe "#validate_declaration_encoding" do
28
+ it "validates encodings" do
29
+ expect { utils.validate_declaration_encoding("UTF-8") }.not_to raise_error
30
+ expect do
31
+ utils.validate_declaration_encoding("ISO-8859-1")
32
+ end.not_to raise_error
33
+ end
34
+ end
35
+
36
+ describe "#validate_element_name" do
37
+ it "validates element names" do
38
+ expect { utils.validate_element_name("root") }.not_to raise_error
39
+ expect { utils.validate_element_name("my-element") }.not_to raise_error
40
+ end
41
+
42
+ it "raises error for invalid names" do
43
+ expect do
44
+ utils.validate_element_name("123invalid")
45
+ end.to raise_error(Moxml::ValidationError,
46
+ "Invalid XML element name: 123invalid")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::AST::Node do
6
+ describe "abstract interface" do
7
+ let(:node) { described_class.new }
8
+
9
+ describe "#evaluate" do
10
+ it "raises NotImplementedError" do
11
+ context = Moxml::XPath::Context.new
12
+ expect { node.evaluate(context) }.to raise_error(
13
+ NotImplementedError,
14
+ /#{described_class}#evaluate must be implemented by subclass/,
15
+ )
16
+ end
17
+ end
18
+
19
+ describe "#constant?" do
20
+ it "returns false by default" do
21
+ expect(node.constant?).to be(false)
22
+ end
23
+ end
24
+
25
+ describe "#result_type" do
26
+ it "returns :unknown by default" do
27
+ expect(node.result_type).to eq(:unknown)
28
+ end
29
+ end
30
+
31
+ describe "#inspect" do
32
+ it "returns class name representation" do
33
+ expect(node.inspect).to match(/^#<#{Regexp.escape(described_class.name)} @type=/)
34
+ end
35
+ end
36
+
37
+ describe "#to_s" do
38
+ it "aliases #inspect" do
39
+ expect(node.to_s).to eq(node.inspect)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "concrete implementation" do
45
+ # Create a test subclass to verify the interface works
46
+ let(:test_class) do
47
+ Class.new(described_class) do
48
+ def initialize(value)
49
+ @value = value
50
+ end
51
+
52
+ def evaluate(_context)
53
+ @value
54
+ end
55
+
56
+ def constant?
57
+ true
58
+ end
59
+
60
+ def result_type
61
+ :string
62
+ end
63
+ end
64
+ end
65
+
66
+ it "can be subclassed with custom evaluate" do
67
+ node = test_class.new("test_value")
68
+ context = Moxml::XPath::Context.new
69
+
70
+ expect(node.evaluate(context)).to eq("test_value")
71
+ end
72
+
73
+ it "can override constant?" do
74
+ node = test_class.new("test")
75
+ expect(node.constant?).to be(true)
76
+ end
77
+
78
+ it "can override result_type" do
79
+ node = test_class.new("test")
80
+ expect(node.result_type).to eq(:string)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XPath Axes" do
6
+ let(:context) { Moxml.new(:nokogiri) }
7
+ let(:doc) do
8
+ xml = <<~XML
9
+ <root>
10
+ <parent id="p1">
11
+ <child id="c1">
12
+ <grandchild id="g1">text1</grandchild>
13
+ </child>
14
+ <child id="c2">
15
+ <grandchild id="g2">text2</grandchild>
16
+ </child>
17
+ </parent>
18
+ </root>
19
+ XML
20
+ context.parse(xml)
21
+ end
22
+
23
+ describe "Implemented Axes" do
24
+ it "lists all implemented axes" do
25
+ compiler = Moxml::XPath::Compiler.new
26
+
27
+ axes = [
28
+ "child", "self", "parent",
29
+ "descendant", "descendant_or_self", "attribute",
30
+ "ancestor", "ancestor_or_self",
31
+ "following_sibling", "preceding_sibling",
32
+ "following", "preceding", "namespace"
33
+ ]
34
+
35
+ implemented = axes.select do |axis|
36
+ compiler.respond_to?(:"on_axis_#{axis}", true)
37
+ end
38
+
39
+ puts "\nImplemented Axes: #{implemented.size}/13"
40
+ implemented.each { |a| puts " ✓ #{a}" }
41
+
42
+ missing = axes - implemented
43
+ if missing.any?
44
+ puts "\nMissing Axes: #{missing.size}/13"
45
+ missing.each { |a| puts " ✗ #{a}" }
46
+ end
47
+
48
+ # Expect at least 6 axes (3 original + 3 new critical axes)
49
+ expect(implemented.size).to be >= 6
50
+ expect(implemented).to include("child", "self", "parent")
51
+ expect(implemented).to include("descendant_or_self", "attribute",
52
+ "descendant")
53
+ end
54
+ end
55
+
56
+ describe "Critical Axes" do
57
+ describe "child axis" do
58
+ it "selects direct children only" do
59
+ ast = Moxml::XPath::Parser.parse("/root/child::parent")
60
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
61
+ result = proc.call(doc)
62
+
63
+ expect(result.size).to eq(1)
64
+ expect(result.first.attribute("id").value).to eq("p1")
65
+ end
66
+
67
+ it "does not select grandchildren" do
68
+ ast = Moxml::XPath::Parser.parse("/root/child::child")
69
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
70
+ result = proc.call(doc)
71
+
72
+ expect(result).to be_empty
73
+ end
74
+ end
75
+
76
+ describe "self axis" do
77
+ it "selects the context node" do
78
+ ast = Moxml::XPath::Parser.parse("/root/self::root")
79
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
80
+ result = proc.call(doc)
81
+
82
+ expect(result.size).to eq(1)
83
+ expect(result.first.name).to eq("root")
84
+ end
85
+ end
86
+
87
+ describe "parent axis" do
88
+ it "selects parent of children" do
89
+ ast = Moxml::XPath::Parser.parse("/root/parent/child/parent::parent")
90
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
91
+ result = proc.call(doc)
92
+
93
+ expect(result.size).to eq(2) # Two children, both have same parent
94
+ expect(result.first.attribute("id").value).to eq("p1")
95
+ end
96
+ end
97
+
98
+ describe "descendant-or-self axis (enables //)" do
99
+ it "finds all descendant nodes including self" do
100
+ ast = Moxml::XPath::Parser.parse("/root/descendant-or-self::root")
101
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
102
+ result = proc.call(doc)
103
+
104
+ expect(result.size).to eq(1)
105
+ expect(result.first.name).to eq("root")
106
+ end
107
+
108
+ it "finds deep descendants" do
109
+ ast = Moxml::XPath::Parser.parse("/root/descendant-or-self::grandchild")
110
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
111
+ result = proc.call(doc)
112
+
113
+ expect(result.size).to eq(2)
114
+ expect(result.map do |n|
115
+ n.attribute("id").value
116
+ end).to contain_exactly("g1", "g2")
117
+ end
118
+
119
+ it "powers the // operator" do
120
+ ast = Moxml::XPath::Parser.parse("//grandchild")
121
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
122
+ result = proc.call(doc)
123
+
124
+ expect(result.size).to eq(2)
125
+ end
126
+ end
127
+
128
+ describe "attribute axis (enables @)" do
129
+ let(:attr_doc) do
130
+ xml = <<~XML
131
+ <root>
132
+ <item id="1" name="first" type="A"/>
133
+ <item id="2" name="second" type="B"/>
134
+ </root>
135
+ XML
136
+ context.parse(xml)
137
+ end
138
+
139
+ it "selects specific attribute" do
140
+ ast = Moxml::XPath::Parser.parse("/root/item/attribute::id")
141
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
142
+ result = proc.call(attr_doc)
143
+
144
+ expect(result.size).to eq(2)
145
+ expect(result.map(&:value)).to contain_exactly("1", "2")
146
+ end
147
+
148
+ it "selects all attributes with wildcard" do
149
+ ast = Moxml::XPath::Parser.parse("/root/item/attribute::*")
150
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
151
+ result = proc.call(attr_doc)
152
+
153
+ # 2 items × 3 attributes each = 6
154
+ expect(result.size).to eq(6)
155
+ end
156
+
157
+ it "powers the @ operator" do
158
+ ast = Moxml::XPath::Parser.parse("/root/item/@name")
159
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
160
+ result = proc.call(attr_doc)
161
+
162
+ expect(result.size).to eq(2)
163
+ expect(result.map(&:value)).to contain_exactly("first", "second")
164
+ end
165
+ end
166
+
167
+ describe "descendant axis" do
168
+ it "finds descendants without self" do
169
+ ast = Moxml::XPath::Parser.parse("/root/descendant::child")
170
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
171
+ result = proc.call(doc)
172
+
173
+ expect(result.size).to eq(2)
174
+ expect(result.map do |n|
175
+ n.attribute("id").value
176
+ end).to contain_exactly("c1", "c2")
177
+ end
178
+
179
+ it "does not include context node" do
180
+ ast = Moxml::XPath::Parser.parse("/root/parent/descendant::parent")
181
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
182
+ result = proc.call(doc)
183
+
184
+ # Should not find parent itself, only descendants named parent
185
+ expect(result).to be_empty
186
+ end
187
+
188
+ it "finds all descendants at any depth" do
189
+ ast = Moxml::XPath::Parser.parse("/root/descendant::*")
190
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
191
+ result = proc.call(doc)
192
+
193
+ # parent + 2 children + 2 grandchildren = 5
194
+ expect(result.size).to eq(5)
195
+ end
196
+ end
197
+ end
198
+
199
+ describe "Axis + Predicate Combinations" do
200
+ let(:book_doc) do
201
+ xml = <<~XML
202
+ <library>
203
+ <book price="10" category="fiction">
204
+ <title>Book A</title>
205
+ </book>
206
+ <book price="20" category="tech">
207
+ <title>Book B</title>
208
+ </book>
209
+ <book price="15" category="fiction">
210
+ <title>Book C</title>
211
+ </book>
212
+ </library>
213
+ XML
214
+ context.parse(xml)
215
+ end
216
+
217
+ it "combines descendant-or-self with element test" do
218
+ ast = Moxml::XPath::Parser.parse("//book")
219
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
220
+ result = proc.call(book_doc)
221
+
222
+ expect(result.size).to eq(3)
223
+ end
224
+
225
+ it "combines attribute axis with wildcards" do
226
+ skip "HeadedOx limitation: Attribute wildcard (@*) not supported by XPath parser. See docs/HEADED_OX_LIMITATIONS.md"
227
+ ast = Moxml::XPath::Parser.parse("//book/@*")
228
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
229
+ result = proc.call(book_doc)
230
+
231
+ # 3 books × 2 attributes each = 6
232
+ expect(result.size).to eq(6)
233
+ end
234
+ end
235
+
236
+ describe "Real-world XPath Patterns" do
237
+ let(:complex_doc) do
238
+ xml = <<~XML
239
+ <catalog>
240
+ <section name="Programming">
241
+ <book id="b1" price="29.99">
242
+ <title lang="en">Ruby Programming</title>
243
+ <author>Matz</author>
244
+ </book>
245
+ <book id="b2" price="39.99">
246
+ <title lang="en">Python Programming</title>
247
+ <author>Guido</author>
248
+ </book>
249
+ </section>
250
+ <section name="Databases">
251
+ <book id="b3" price="49.99">
252
+ <title lang="en">PostgreSQL Essentials</title>
253
+ <author>Expert</author>
254
+ </book>
255
+ </section>
256
+ </catalog>
257
+ XML
258
+ context.parse(xml)
259
+ end
260
+
261
+ it "finds all books anywhere //book" do
262
+ ast = Moxml::XPath::Parser.parse("//book")
263
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
264
+ result = proc.call(complex_doc)
265
+
266
+ expect(result.size).to eq(3)
267
+ end
268
+
269
+ it "finds all titles anywhere //title" do
270
+ ast = Moxml::XPath::Parser.parse("//title")
271
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
272
+ result = proc.call(complex_doc)
273
+
274
+ expect(result.size).to eq(3)
275
+ expect(result.map(&:text)).to include("Ruby Programming",
276
+ "Python Programming", "PostgreSQL Essentials")
277
+ end
278
+
279
+ it "finds all price attributes //book/@price" do
280
+ ast = Moxml::XPath::Parser.parse("//book/@price")
281
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
282
+ result = proc.call(complex_doc)
283
+
284
+ expect(result.size).to eq(3)
285
+ expect(result.map(&:value)).to contain_exactly("29.99", "39.99", "49.99")
286
+ end
287
+
288
+ it "combines // with specific paths //section/book" do
289
+ ast = Moxml::XPath::Parser.parse("//section/book")
290
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
291
+ result = proc.call(complex_doc)
292
+
293
+ expect(result.size).to eq(3)
294
+ end
295
+ end
296
+ end