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,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Ensure HeadedOx adapter is loaded
6
+ Moxml::Adapter.load(:headed_ox)
7
+
8
+ RSpec.describe Moxml::Adapter::HeadedOx do
9
+ let(:adapter) { described_class }
10
+ let(:xml) do
11
+ <<~XML
12
+ <root>
13
+ <book price="10">
14
+ <title>Book 1</title>
15
+ <author>Author A</author>
16
+ </book>
17
+ <book price="20">
18
+ <title>Book 2</title>
19
+ <author>Author B</author>
20
+ </book>
21
+ <book price="30">
22
+ <title>Book 3</title>
23
+ <author>Author C</author>
24
+ </book>
25
+ </root>
26
+ XML
27
+ end
28
+
29
+ describe ".parse" do
30
+ it "parses XML using Ox" do
31
+ doc = adapter.parse(xml)
32
+
33
+ expect(doc).to be_a(Moxml::Document)
34
+ expect(doc.root.name).to eq("root")
35
+ end
36
+ end
37
+
38
+ describe ".xpath" do
39
+ let(:doc) { adapter.parse(xml) }
40
+
41
+ it "executes simple XPath queries" do
42
+ result = adapter.xpath(doc, "/root/book")
43
+
44
+ expect(result).to be_a(Moxml::NodeSet)
45
+ expect(result.size).to eq(3)
46
+ end
47
+
48
+ it "executes XPath with numeric predicates" do
49
+ result = adapter.xpath(doc, "//book[@price < 25]")
50
+
51
+ expect(result.size).to eq(2)
52
+ end
53
+
54
+ it "executes XPath with string predicates" do
55
+ result = adapter.xpath(doc, "//book[@price='20']")
56
+
57
+ expect(result.size).to eq(1)
58
+ end
59
+
60
+ it "executes XPath with functions" do
61
+ result = adapter.xpath(doc, "count(//book)")
62
+
63
+ expect(result).to eq(3.0)
64
+ end
65
+
66
+ it "executes complex XPath with paths" do
67
+ result = adapter.xpath(doc, "//book[@price < 25]/title")
68
+
69
+ expect(result.size).to eq(2)
70
+ expect(result.map(&:text)).to contain_exactly("Book 1", "Book 2")
71
+ end
72
+
73
+ it "supports XPath string functions in predicates" do
74
+ skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
75
+ result = adapter.xpath(doc, "//book[contains(title, '2')]")
76
+
77
+ expect(result.size).to eq(1)
78
+ expect(result.first.xpath("title").first.text).to eq("Book 2")
79
+ end
80
+
81
+ it "supports XPath position functions" do
82
+ skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
83
+ result = adapter.xpath(doc, "//book[position() = 2]")
84
+
85
+ expect(result.size).to eq(1)
86
+ expect(result.first.xpath("title").first.text).to eq("Book 2")
87
+ end
88
+
89
+ it "supports descendant axis" do
90
+ result = adapter.xpath(doc, "//title")
91
+
92
+ expect(result.size).to eq(3)
93
+ end
94
+
95
+ it "supports attribute axis" do
96
+ result = adapter.xpath(doc, "//book/@price")
97
+
98
+ expect(result.size).to eq(3)
99
+ expect(result.map(&:value)).to contain_exactly("10", "20", "30")
100
+ end
101
+
102
+ it "supports parent axis" do
103
+ title = adapter.xpath(doc, "//title").first
104
+ result = adapter.xpath(title, "parent::book")
105
+
106
+ expect(result.size).to eq(1)
107
+ expect(result.first.name).to eq("book")
108
+ end
109
+
110
+ it "handles namespace queries" do
111
+ ns_xml = '<root xmlns:ns="http://example.com"><ns:item/></root>'
112
+ ns_doc = adapter.parse(ns_xml)
113
+
114
+ result = adapter.xpath(
115
+ ns_doc,
116
+ "//ns:item",
117
+ { "ns" => "http://example.com" },
118
+ )
119
+
120
+ expect(result.size).to eq(1)
121
+ end
122
+
123
+ it "returns non-node values directly" do
124
+ result = adapter.xpath(doc, "string(//book[1]/title)")
125
+
126
+ expect(result).to eq("Book 1")
127
+ end
128
+
129
+ it "handles boolean results" do
130
+ result = adapter.xpath(doc, "boolean(//book)")
131
+
132
+ expect(result).to be true
133
+ end
134
+
135
+ it "wraps error with XPathError" do
136
+ expect do
137
+ adapter.xpath(doc, "invalid[[[syntax")
138
+ end.to raise_error(Moxml::XPathError)
139
+ end
140
+ end
141
+
142
+ describe ".at_xpath" do
143
+ let(:doc) { adapter.parse(xml) }
144
+
145
+ it "returns first matching node" do
146
+ result = adapter.at_xpath(doc, "//book")
147
+
148
+ expect(result).to be_a(Moxml::Element)
149
+ expect(result.name).to eq("book")
150
+ end
151
+
152
+ it "returns nil when no match" do
153
+ result = adapter.at_xpath(doc, "//nonexistent")
154
+
155
+ expect(result).to be_nil
156
+ end
157
+
158
+ it "returns scalar values directly" do
159
+ result = adapter.at_xpath(doc, "count(//book)")
160
+
161
+ expect(result).to eq(3.0)
162
+ end
163
+ end
164
+
165
+ describe ".xpath_supported?" do
166
+ it "returns true" do
167
+ expect(adapter.xpath_supported?).to be true
168
+ end
169
+ end
170
+
171
+ describe ".capabilities" do
172
+ it "reports full XPath support" do
173
+ caps = adapter.capabilities
174
+
175
+ expect(caps[:xpath_full]).to be true
176
+ expect(caps[:xpath_axes]).to eq(:partial) # 6 of 13 axes supported
177
+ expect(caps[:xpath_functions]).to eq(:complete) # All 27 XPath 1.0 functions
178
+ expect(caps[:xpath_predicates]).to be true
179
+ expect(caps[:xpath_namespaces]).to be true
180
+ expect(caps[:xpath_variables]).to be true
181
+ end
182
+
183
+ it "inherits Ox adapter capabilities" do
184
+ caps = adapter.capabilities
185
+
186
+ # Should have parsing capability from Ox
187
+ expect(caps).to have_key(:parse)
188
+ end
189
+ end
190
+
191
+ describe "XPath function support" do
192
+ let(:doc) { adapter.parse(xml) }
193
+
194
+ context "string functions" do
195
+ it "supports string()" do
196
+ result = adapter.xpath(doc, "string(//book[1]/title)")
197
+ expect(result).to eq("Book 1")
198
+ end
199
+
200
+ it "supports concat()" do
201
+ result = adapter.xpath(doc, "concat('Price: ', //book[1]/@price)")
202
+ expect(result).to eq("Price: 10")
203
+ end
204
+
205
+ it "supports contains()" do
206
+ result = adapter.xpath(doc, "//book[contains(title, 'Book')]")
207
+ expect(result.size).to eq(3)
208
+ end
209
+
210
+ it "supports starts-with()" do
211
+ result = adapter.xpath(doc, "//book[starts-with(title, 'Book')]")
212
+ expect(result.size).to eq(3)
213
+ end
214
+
215
+ it "supports substring()" do
216
+ result = adapter.xpath(doc, "substring('Hello World', 7)")
217
+ expect(result).to eq("World")
218
+ end
219
+
220
+ it "supports string-length()" do
221
+ result = adapter.xpath(doc, "string-length('Hello')")
222
+ expect(result).to eq(5.0)
223
+ end
224
+
225
+ it "supports normalize-space()" do
226
+ result = adapter.xpath(doc, "normalize-space(' hello world ')")
227
+ expect(result).to eq("hello world")
228
+ end
229
+ end
230
+
231
+ context "numeric functions" do
232
+ it "supports number()" do
233
+ result = adapter.xpath(doc, "number(//book[1]/@price)")
234
+ expect(result).to eq(10.0)
235
+ end
236
+
237
+ it "supports sum()" do
238
+ result = adapter.xpath(doc, "sum(//book/@price)")
239
+ expect(result).to eq(60.0)
240
+ end
241
+
242
+ it "supports count()" do
243
+ result = adapter.xpath(doc, "count(//book)")
244
+ expect(result).to eq(3.0)
245
+ end
246
+
247
+ it "supports floor()" do
248
+ result = adapter.xpath(doc, "floor(3.7)")
249
+ expect(result).to eq(3.0)
250
+ end
251
+
252
+ it "supports ceiling()" do
253
+ result = adapter.xpath(doc, "ceiling(3.2)")
254
+ expect(result).to eq(4.0)
255
+ end
256
+
257
+ it "supports round()" do
258
+ result = adapter.xpath(doc, "round(3.5)")
259
+ expect(result).to eq(4.0)
260
+ end
261
+ end
262
+
263
+ context "boolean functions" do
264
+ it "supports boolean()" do
265
+ result = adapter.xpath(doc, "boolean(//book)")
266
+ expect(result).to be true
267
+ end
268
+
269
+ it "supports not()" do
270
+ result = adapter.xpath(doc, "not(false())")
271
+ expect(result).to be true
272
+ end
273
+
274
+ it "supports true()" do
275
+ result = adapter.xpath(doc, "true()")
276
+ expect(result).to be true
277
+ end
278
+
279
+ it "supports false()" do
280
+ result = adapter.xpath(doc, "false()")
281
+ expect(result).to be false
282
+ end
283
+ end
284
+
285
+ context "node functions" do
286
+ it "supports name()" do
287
+ result = adapter.xpath(doc, "name(//book[1])")
288
+ expect(result).to eq("book")
289
+ end
290
+
291
+ it "supports local-name()" do
292
+ result = adapter.xpath(doc, "local-name(//book[1])")
293
+ expect(result).to eq("book")
294
+ end
295
+ end
296
+
297
+ context "position functions" do
298
+ it "supports position()" do
299
+ result = adapter.xpath(doc, "//book[position() = 2]")
300
+ expect(result.size).to eq(1)
301
+ end
302
+
303
+ it "supports last()" do
304
+ skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
305
+ result = adapter.xpath(doc, "//book[position() = last()]")
306
+ expect(result.size).to eq(1)
307
+ expect(result.first.xpath("title").first.text).to eq("Book 3")
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libxml"
4
+ require "moxml/adapter/libxml"
5
+
6
+ RSpec.describe Moxml::Adapter::Libxml do
7
+ around do |example|
8
+ Moxml.with_config(:libxml, true, "UTF-8") do
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it_behaves_like "xml adapter"
14
+ end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ox"
4
3
  require "moxml/adapter/ox"
5
4
 
6
- RSpec.describe Moxml::Adapter::Ox, skip: "Ox will be added later" do
7
- before(:all) do
8
- Moxml.configure do |config|
9
- config.adapter = :ox
10
- config.strict_parsing = true
11
- config.default_encoding = "UTF-8"
5
+ RSpec.describe Moxml::Adapter::Ox do
6
+ around do |example|
7
+ Moxml.with_config(:ox, true, "UTF-8") do
8
+ example.run
12
9
  end
13
10
  end
14
11
 
@@ -32,7 +29,9 @@ RSpec.describe Moxml::Adapter::Ox, skip: "Ox will be added later" do
32
29
  end
33
30
 
34
31
  describe "xpath support" do
35
- let(:doc) { described_class.parse("<root><child id='1'>text</child></root>") }
32
+ let(:doc) do
33
+ described_class.parse("<root><child id='1'>text</child></root>").native
34
+ end
36
35
 
37
36
  it "supports basic element matching" do
38
37
  nodes = described_class.xpath(doc, "//child")
@@ -41,6 +40,8 @@ RSpec.describe Moxml::Adapter::Ox, skip: "Ox will be added later" do
41
40
  end
42
41
 
43
42
  it "supports attribute matching" do
43
+ pending("Ox does not support attribute value predicates in XPath (documented limitation)")
44
+
44
45
  nodes = described_class.xpath(doc, "//child[@id='1']")
45
46
  expect(nodes.size).to eq(1)
46
47
  expect(nodes.first.attributes["id"]).to eq("1")
File without changes
@@ -26,6 +26,9 @@ RSpec.shared_examples "xml adapter" do
26
26
  end
27
27
 
28
28
  it "handles malformed XML according to strict setting" do
29
+ if described_class.name.include?("Ox")
30
+ skip("Ox does not support non-strict parsing mode")
31
+ end
29
32
  malformed = "<root><unclosed>"
30
33
 
31
34
  expect do
@@ -164,8 +167,18 @@ RSpec.shared_examples "xml adapter" do
164
167
  end
165
168
 
166
169
  it "respects indentation settings" do
167
- pending("Oga does not support indentation settings") if described_class.name.include?("Oga")
168
- pending("Postponed for Rexml till better times") if described_class.name.include?("Rexml")
170
+ if described_class.name.include?("Oga")
171
+ pending("Oga does not support indentation settings")
172
+ end
173
+ if described_class.name.include?("Rexml")
174
+ pending("Postponed for Rexml till better times")
175
+ end
176
+ if described_class.name.include?("Libxml")
177
+ skip("LibXML serialization does not support indentation (documented limitation)")
178
+ end
179
+ if described_class.name.include?("Ox")
180
+ skip("Ox does not support indentation settings (documented limitation)")
181
+ end
169
182
 
170
183
  unindented = described_class.serialize(doc, indent: 0)
171
184
  indented = described_class.serialize(doc, indent: 2)
@@ -217,7 +230,9 @@ RSpec.shared_examples "xml adapter" do
217
230
  end
218
231
 
219
232
  it "preserves and correctly handles multiple namespaces" do
220
- pending("Rexml does not respect ZPath namespaces") if described_class.name.include?("Rexml")
233
+ if described_class.name.include?("Rexml")
234
+ pending("Rexml does not respect ZPath namespaces")
235
+ end
221
236
  # Parse original XML
222
237
  doc = described_class.parse(xml).native
223
238
 
@@ -229,18 +244,21 @@ RSpec.shared_examples "xml adapter" do
229
244
  # Test xpath with namespaces
230
245
  namespaces = {
231
246
  "svg" => "http://www.w3.org/2000/svg",
232
- "xlink" => "http://www.w3.org/1999/xlink"
247
+ "xlink" => "http://www.w3.org/1999/xlink",
233
248
  }
234
249
 
235
250
  # Find use element and verify xlink:href attribute
236
251
  use_elem = described_class.at_xpath(doc, "//svg:use", namespaces)
237
252
  expect(use_elem).not_to be_nil
238
- expect(described_class.get_attribute_value(use_elem, "xlink:href")).to eq("#myCircle")
253
+ expect(described_class.get_attribute_value(use_elem,
254
+ "xlink:href")).to eq("#myCircle")
239
255
 
240
256
  # Verify circle element exists in defs
241
- circle = described_class.at_xpath(doc, "//svg:defs/svg:circle", namespaces)
257
+ circle = described_class.at_xpath(doc, "//svg:defs/svg:circle",
258
+ namespaces)
242
259
  expect(circle).not_to be_nil
243
- expect(described_class.get_attribute_value(circle, "id")).to eq("myCircle")
260
+ expect(described_class.get_attribute_value(circle,
261
+ "id")).to eq("myCircle")
244
262
 
245
263
  # Test default SVG namespace
246
264
  text = described_class.at_xpath(doc, "//svg:text", namespaces)
@@ -252,7 +270,7 @@ RSpec.shared_examples "xml adapter" do
252
270
  let(:xml) do
253
271
  <<~XML
254
272
  <?xml version="1.0" encoding="UTF-8"?>
255
- <rss version="2.0"#{" "}
273
+ <rss version="2.0"#{' '}
256
274
  xmlns:atom="http://www.w3.org/2005/Atom"
257
275
  xmlns:dc="http://purl.org/dc/elements/1.1/"
258
276
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
@@ -270,6 +288,10 @@ RSpec.shared_examples "xml adapter" do
270
288
  end
271
289
 
272
290
  it "preserves and correctly handles multiple namespaces" do
291
+ if described_class.name.include?("Ox")
292
+ skip("Ox does not support namespace-aware XPath queries (documented limitation)")
293
+ end
294
+
273
295
  # Parse original XML
274
296
  doc = described_class.parse(xml).native
275
297
 
@@ -283,7 +305,7 @@ RSpec.shared_examples "xml adapter" do
283
305
  namespaces = {
284
306
  "atom" => "http://www.w3.org/2005/Atom",
285
307
  "dc" => "http://purl.org/dc/elements/1.1/",
286
- "content" => "http://purl.org/rss/1.0/modules/content/"
308
+ "content" => "http://purl.org/rss/1.0/modules/content/",
287
309
  }
288
310
 
289
311
  # Find creator using namespaced xpath
@@ -305,7 +327,7 @@ RSpec.shared_examples "xml adapter" do
305
327
  let(:xml) do
306
328
  <<~XML
307
329
  <?xml version="1.0" encoding="UTF-8"?>
308
- <soap:Envelope#{" "}
330
+ <soap:Envelope#{' '}
309
331
  xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
310
332
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
311
333
  xmlns:ns="urn:example:namespace">
@@ -322,6 +344,10 @@ RSpec.shared_examples "xml adapter" do
322
344
  end
323
345
 
324
346
  it "preserves and correctly handles multiple namespaces" do
347
+ if described_class.name.include?("Ox")
348
+ skip("Ox does not support namespace-aware XPath queries (documented limitation)")
349
+ end
350
+
325
351
  # Parse original XML
326
352
  doc = described_class.parse(xml).native
327
353
 
@@ -334,7 +360,7 @@ RSpec.shared_examples "xml adapter" do
334
360
  # Test xpath with namespaces
335
361
  namespaces = {
336
362
  "soap" => "http://www.w3.org/2003/05/soap-envelope",
337
- "ns" => "urn:example:namespace"
363
+ "ns" => "urn:example:namespace",
338
364
  }
339
365
 
340
366
  # Find user ID using namespaced xpath
@@ -346,7 +372,8 @@ RSpec.shared_examples "xml adapter" do
346
372
  expect(body).not_to be_nil
347
373
 
348
374
  # Verify attribute with namespace
349
- expect(described_class.get_attribute_value(user_id, "xsi:type")).to eq("xsi:string")
375
+ expect(described_class.get_attribute_value(user_id,
376
+ "xsi:type")).to eq("xsi:string")
350
377
  end
351
378
  end
352
379
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Adapter do
6
+ describe "adapter loading" do
7
+ it "provides Nokogiri adapter" do
8
+ expect(described_class::Nokogiri).to be_a(Class)
9
+ expect(described_class::Nokogiri.ancestors).to include(Moxml::Adapter::Base)
10
+ end
11
+
12
+ it "provides Base adapter class" do
13
+ expect(described_class::Base).to be_a(Class)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Attribute do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse('<root id="123" class="test"/>') }
8
+ let(:element) { doc.root }
9
+
10
+ describe "#name" do
11
+ it "returns attribute name" do
12
+ attr = element.attributes.first
13
+ expect(%w[id class]).to include(attr.name)
14
+ end
15
+ end
16
+
17
+ describe "#value" do
18
+ it "returns attribute value" do
19
+ id_attr = element.attributes.find { |a| a.name == "id" }
20
+ expect(id_attr.value).to eq("123")
21
+ end
22
+ end
23
+
24
+ describe "#to_s" do
25
+ it "returns string representation" do
26
+ attr = element.attributes.first
27
+ expect(attr.to_s).to match(/\w+="\w+"/)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Builder do
6
+ let(:context) { Moxml.new }
7
+
8
+ describe "#build" do
9
+ it "builds a document with DSL" do
10
+ doc = described_class.new(context).build do
11
+ element "root" do
12
+ element "child" do
13
+ text "text"
14
+ end
15
+ end
16
+ end
17
+
18
+ expect(doc).to be_a(Moxml::Document)
19
+ expect(doc.root.name).to eq("root")
20
+ end
21
+
22
+ it "creates nested elements" do
23
+ doc = described_class.new(context).build do
24
+ element "parent" do
25
+ element "child1"
26
+ element "child2"
27
+ end
28
+ end
29
+
30
+ expect(doc.root.children.length).to eq(2)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Cdata do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root><![CDATA[test data]]></root>") }
8
+
9
+ describe "#content" do
10
+ it "returns CDATA content" do
11
+ cdata = doc.root.children.first
12
+ expect(cdata).to be_a(described_class)
13
+ expect(cdata.content).to eq("test data")
14
+ end
15
+ end
16
+
17
+ describe "#to_xml" do
18
+ it "serializes to CDATA section" do
19
+ cdata = doc.root.children.first
20
+ expect(cdata.to_xml).to eq("<![CDATA[test data]]>")
21
+ end
22
+ end
23
+
24
+ describe "creation" do
25
+ it "creates a CDATA node" do
26
+ cdata = doc.create_cdata("new data")
27
+ expect(cdata).to be_a(described_class)
28
+ expect(cdata.content).to eq("new data")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Comment do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root><!-- test comment --></root>") }
8
+
9
+ describe "#content" do
10
+ it "returns comment content" do
11
+ comment = doc.root.children.first
12
+ expect(comment).to be_a(described_class)
13
+ expect(comment.content).to eq(" test comment ")
14
+ end
15
+ end
16
+
17
+ describe "#to_xml" do
18
+ it "serializes to XML comment" do
19
+ comment = doc.root.children.first
20
+ expect(comment.to_xml).to eq("<!-- test comment -->")
21
+ end
22
+ end
23
+
24
+ describe "creation" do
25
+ it "creates a comment node" do
26
+ comment = doc.create_comment("new comment")
27
+ expect(comment).to be_a(described_class)
28
+ expect(comment.content).to eq("new comment")
29
+ end
30
+ end
31
+ end
@@ -21,18 +21,18 @@ RSpec.describe Moxml::Config do
21
21
  end
22
22
 
23
23
  it "raises error for invalid adapter" do
24
- expect { config.adapter = :invalid }.to raise_error(ArgumentError)
24
+ expect { config.adapter = :invalid }.to raise_error(Moxml::AdapterError)
25
25
  end
26
26
 
27
27
  it "requires adapter gem" do
28
28
  expect { config.adapter = :oga }.not_to raise_error
29
29
 
30
- expect(defined?(::Oga)).to be_truthy
30
+ expect(defined?(Oga)).to be_truthy
31
31
  end
32
32
 
33
33
  it "handles missing gems" do
34
34
  allow(Moxml::Adapter).to receive(:require).and_raise(LoadError)
35
- expect { config.adapter = :nokogiri }.to raise_error(LoadError)
35
+ expect { config.adapter = :nokogiri }.to raise_error(Moxml::AdapterError)
36
36
  end
37
37
  end
38
38