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,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module XPath
5
+ module Ruby
6
+ # Class for converting a Ruby AST to a String.
7
+ #
8
+ # This class takes a {Moxml::XPath::Ruby::Node} instance and converts it
9
+ # (and its child nodes) to a String that can be passed to `eval`.
10
+ #
11
+ # @private
12
+ class Generator
13
+ # @param [Moxml::XPath::Ruby::Node] ast
14
+ # @return [String]
15
+ def process(ast)
16
+ handler = :"on_#{ast.type}"
17
+ unless respond_to?(handler, true)
18
+ raise NotImplementedError,
19
+ "Generator missing handler for node type :#{ast.type}. Node: #{ast.inspect}"
20
+ end
21
+
22
+ send(handler, ast)
23
+ end
24
+
25
+ # @param [Moxml::XPath::Ruby::Node] ast
26
+ # @return [String]
27
+ def on_followed_by(ast)
28
+ ast.to_a.map { |child| process(child) }.join("\n\n")
29
+ end
30
+
31
+ # Processes an assignment node.
32
+ #
33
+ # @param [Moxml::XPath::Ruby::Node] ast
34
+ # @return [String]
35
+ def on_assign(ast)
36
+ var, val = *ast
37
+
38
+ var_str = process(var)
39
+ val_str = process(val)
40
+
41
+ "#{var_str} = #{val_str}"
42
+ end
43
+
44
+ # Processes a mass assignment node.
45
+ #
46
+ # @param [Moxml::XPath::Ruby::Node] ast
47
+ # @return [String]
48
+ def on_massign(ast)
49
+ vars, val = *ast
50
+
51
+ var_names = vars.map { |var| process(var) }
52
+ val_str = process(val)
53
+
54
+ "#{var_names.join(', ')} = #{val_str}"
55
+ end
56
+
57
+ # Processes a `begin` node.
58
+ #
59
+ # @param [Moxml::XPath::Ruby::Node] ast
60
+ # @return [String]
61
+ def on_begin(ast)
62
+ body = process(ast.to_a[0])
63
+
64
+ <<~RUBY
65
+ begin
66
+ #{body}
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ # Processes an equality node.
72
+ #
73
+ # @param [Moxml::XPath::Ruby::Node] ast
74
+ # @return [String]
75
+ def on_eq(ast)
76
+ left, right = *ast
77
+
78
+ left_str = process(left)
79
+ right_str = process(right)
80
+
81
+ "#{left_str} == #{right_str}"
82
+ end
83
+
84
+ # Processes a boolean "and" node.
85
+ #
86
+ # @param [Moxml::XPath::Ruby::Node] ast
87
+ # @return [String]
88
+ def on_and(ast)
89
+ left, right = *ast
90
+
91
+ left_str = process(left)
92
+ right_str = process(right)
93
+
94
+ "#{left_str} && #{right_str}"
95
+ end
96
+
97
+ # Processes a boolean "or" node.
98
+ #
99
+ # @param [Moxml::XPath::Ruby::Node] ast
100
+ # @return [String]
101
+ def on_or(ast)
102
+ left, right = *ast
103
+
104
+ left_str = process(left)
105
+ right_str = process(right)
106
+
107
+ "(#{left_str} || #{right_str})"
108
+ end
109
+
110
+ # Processes an if statement node.
111
+ #
112
+ # @param [Moxml::XPath::Ruby::Node] ast
113
+ # @return [String]
114
+ def on_if(ast)
115
+ cond, body, else_body = *ast
116
+
117
+ cond_str = process(cond)
118
+ body_str = process(body)
119
+
120
+ if else_body
121
+ else_str = process(else_body)
122
+
123
+ <<~RUBY
124
+ if #{cond_str}
125
+ #{body_str}
126
+ else
127
+ #{else_str}
128
+ end
129
+ RUBY
130
+ else
131
+ <<~RUBY
132
+ if #{cond_str}
133
+ #{body_str}
134
+ end
135
+ RUBY
136
+ end
137
+ end
138
+
139
+ # Processes a while statement node.
140
+ #
141
+ # @param [Moxml::XPath::Ruby::Node] ast
142
+ # @return [String]
143
+ def on_while(ast)
144
+ cond, body = *ast
145
+
146
+ cond_str = process(cond)
147
+ body_str = process(body)
148
+
149
+ <<~RUBY
150
+ while #{cond_str}
151
+ #{body_str}
152
+ end
153
+ RUBY
154
+ end
155
+
156
+ # Processes a method call node.
157
+ #
158
+ # @param [Moxml::XPath::Ruby::Node] ast
159
+ # @return [String]
160
+ def on_send(ast)
161
+ children = ast.to_a
162
+ receiver = children[0]
163
+ name = children[1]
164
+ args = children[2..] || []
165
+
166
+ call = name
167
+ brackets = name == "[]"
168
+
169
+ unless args.empty?
170
+ arg_strs = []
171
+ args.each do |arg|
172
+ result = process(arg)
173
+ # Keep processing if we got a Node back (happens with nested send nodes)
174
+ while result.respond_to?(:type)
175
+ result = process(result)
176
+ end
177
+ arg_strs << result
178
+ end
179
+ arg_str = arg_strs.join(", ")
180
+ call = brackets ? "[#{arg_str}]" : "#{call}(#{arg_str})"
181
+ end
182
+
183
+ if receiver
184
+ rec_str = process(receiver)
185
+ # Keep processing if we got a Node back
186
+ while rec_str.respond_to?(:type)
187
+ rec_str = process(rec_str)
188
+ end
189
+ call = brackets ? "#{rec_str}#{call}" : "#{rec_str}.#{call}"
190
+ end
191
+
192
+ call
193
+ end
194
+
195
+ # Processes a block node.
196
+ #
197
+ # @param [Moxml::XPath::Ruby::Node] ast
198
+ # @return [String]
199
+ def on_block(ast)
200
+ receiver, args, body = *ast
201
+
202
+ receiver_str = process(receiver)
203
+ body_str = body ? process(body) : nil
204
+ arg_strs = args.map { |arg| process(arg) }
205
+
206
+ <<~RUBY
207
+ #{receiver_str} do |#{arg_strs.join(', ')}|
208
+ #{body_str}
209
+ end
210
+ RUBY
211
+ end
212
+
213
+ # Processes a Range node.
214
+ #
215
+ # @param [Moxml::XPath::Ruby::Node] ast
216
+ # @return [String]
217
+ def on_range(ast)
218
+ start, stop = *ast
219
+
220
+ start_str = process(start)
221
+ stop_str = process(stop)
222
+
223
+ "(#{start_str}..#{stop_str})"
224
+ end
225
+
226
+ # Processes a string node.
227
+ #
228
+ # @param [Moxml::XPath::Ruby::Node] ast
229
+ # @return [String]
230
+ def on_string(ast)
231
+ ast.to_a[0].inspect
232
+ end
233
+
234
+ # Processes a Symbol node.
235
+ #
236
+ # @param [Moxml::XPath::Ruby::Node] ast
237
+ # @return [String]
238
+ def on_symbol(ast)
239
+ ast.to_a[0].to_sym.inspect
240
+ end
241
+
242
+ # Processes a literal node.
243
+ #
244
+ # @param [Moxml::XPath::Ruby::Node] ast
245
+ # @return [String]
246
+ def on_lit(ast)
247
+ ast.to_a[0]
248
+ end
249
+
250
+ # Processes a constant reference node (e.g., Moxml::Document).
251
+ #
252
+ # @param [Moxml::XPath::Ruby::Node] ast
253
+ # @return [String]
254
+ def on_const(ast)
255
+ ast.to_a.join("::")
256
+ end
257
+
258
+ # Processes an array literal node.
259
+ #
260
+ # @param [Moxml::XPath::Ruby::Node] ast
261
+ # @return [String]
262
+ def on_array(ast)
263
+ elements = ast.to_a.map { |elem| process(elem) }
264
+ "[#{elements.join(', ')}]"
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module XPath
5
+ module Ruby
6
+ # Class representing a single node in a Ruby AST.
7
+ #
8
+ # This class provides a fluent DSL for building Ruby code dynamically.
9
+ # It's modeled after the "ast" gem but simplified to avoid method conflicts.
10
+ #
11
+ # @example Building an if statement
12
+ # number1 = Node.new(:lit, ['10'])
13
+ # number2 = Node.new(:lit, ['20'])
14
+ #
15
+ # (number2 > number1).if_true do
16
+ # Node.new(:lit, ['30'])
17
+ # end
18
+ #
19
+ # @private
20
+ class Node < BasicObject
21
+ undef_method :!, :!=
22
+
23
+ # @return [Symbol]
24
+ attr_reader :type
25
+
26
+ # @param [Symbol] type The type of AST node
27
+ # @param [Array] children Child nodes or values
28
+ def initialize(type, children = [])
29
+ @type = type.to_sym
30
+ @children = children
31
+ end
32
+
33
+ # @return [Array]
34
+ def to_a
35
+ @children
36
+ end
37
+ alias to_ary to_a
38
+
39
+ # Returns a "to_a" call node.
40
+ #
41
+ # @return [Moxml::XPath::Ruby::Node]
42
+ def to_array
43
+ Node.new(:send, [self, :to_a])
44
+ end
45
+
46
+ # Returns an assignment node.
47
+ #
48
+ # Wraps assigned values in a begin/end block to ensure that
49
+ # multiple lines of code result in the proper value being assigned.
50
+ #
51
+ # @param [Moxml::XPath::Ruby::Node] other
52
+ # @return [Moxml::XPath::Ruby::Node]
53
+ def assign(other)
54
+ other = other.wrap if other.type == :followed_by
55
+
56
+ Node.new(:assign, [self, other])
57
+ end
58
+
59
+ # Returns an equality expression node.
60
+ #
61
+ # @param [Moxml::XPath::Ruby::Node] other
62
+ # @return [Moxml::XPath::Ruby::Node]
63
+ def eq(other)
64
+ Node.new(:eq, [self, other])
65
+ end
66
+
67
+ # Returns a boolean "and" node.
68
+ #
69
+ # @param [Moxml::XPath::Ruby::Node] other
70
+ # @return [Moxml::XPath::Ruby::Node]
71
+ def and(other)
72
+ Node.new(:and, [self, other])
73
+ end
74
+
75
+ # Returns a boolean "or" node.
76
+ #
77
+ # @param [Moxml::XPath::Ruby::Node] other
78
+ # @return [Moxml::XPath::Ruby::Node]
79
+ def or(other)
80
+ Node.new(:or, [self, other])
81
+ end
82
+
83
+ # Returns a node that evaluates to its inverse.
84
+ #
85
+ # @example
86
+ # foo.not # => !foo
87
+ #
88
+ # @return [Moxml::XPath::Ruby::Node]
89
+ def not
90
+ !self
91
+ end
92
+
93
+ # Returns a node for Ruby's "is_a?" method.
94
+ #
95
+ # @param [Class] klass
96
+ # @return [Moxml::XPath::Ruby::Node]
97
+ def is_a?(klass)
98
+ # If klass is already a Node (e.g., a const node), use it directly
99
+ # Otherwise wrap it in a lit node
100
+ klass_node = if klass.respond_to?(:type)
101
+ klass
102
+ else
103
+ Node.new(:lit, [klass.to_s])
104
+ end
105
+
106
+ Node.new(:send, [self, "is_a?", klass_node])
107
+ end
108
+
109
+ # Wraps the current node in a block.
110
+ #
111
+ # @param [Array] args Arguments (as Node instances) to pass to the block
112
+ # @return [Moxml::XPath::Ruby::Node]
113
+ def add_block(*args)
114
+ Node.new(:block, [self, args, yield])
115
+ end
116
+
117
+ # Wraps the current node in a `begin` node.
118
+ #
119
+ # @return [Moxml::XPath::Ruby::Node]
120
+ def wrap
121
+ Node.new(:begin, [self])
122
+ end
123
+
124
+ # Wraps the current node in an if statement node.
125
+ #
126
+ # The body of this statement is set to the return value of the supplied
127
+ # block.
128
+ #
129
+ # @return [Moxml::XPath::Ruby::Node]
130
+ def if_true
131
+ Node.new(:if, [self, yield])
132
+ end
133
+
134
+ # Wraps the current node in an `if !...` statement.
135
+ #
136
+ # @see [#if_true]
137
+ def if_false(&block)
138
+ self.not.if_true(&block)
139
+ end
140
+
141
+ # Wraps the current node in a `while` statement.
142
+ #
143
+ # The body of this statement is set to the return value of the supplied
144
+ # block.
145
+ #
146
+ # @return [Moxml::XPath::Ruby::Node]
147
+ def while_true
148
+ Node.new(:while, [self, yield])
149
+ end
150
+
151
+ # Adds an "else" statement to the current node.
152
+ #
153
+ # This method assumes it's being called only on "if" nodes.
154
+ #
155
+ # @return [Moxml::XPath::Ruby::Node]
156
+ def else
157
+ Node.new(:if, @children + [yield])
158
+ end
159
+
160
+ # Chains two nodes together.
161
+ #
162
+ # @param [Moxml::XPath::Ruby::Node] other
163
+ # @return [Moxml::XPath::Ruby::Node]
164
+ def followed_by(other = nil)
165
+ other = yield if ::Kernel.block_given?
166
+
167
+ Node.new(:followed_by, [self, other])
168
+ end
169
+
170
+ # Returns a node for a method call.
171
+ #
172
+ # @param [Symbol] name The name of the method to call
173
+ # @param [Array] args Any arguments (as Node instances) to pass
174
+ # @return [Moxml::XPath::Ruby::Node]
175
+ def method_missing(name, *args)
176
+ Node.new(:send, [self, name.to_s, *args])
177
+ end
178
+
179
+ # Prevent implicit string conversion - Nodes must be explicitly processed
180
+ def to_str
181
+ ::Kernel.raise ::TypeError, "Cannot implicitly
182
+
183
+ convert #{self.class} to String. Use Generator#process instead."
184
+ end
185
+
186
+ # @return [String]
187
+ def inspect
188
+ "(#{type} #{@children.map(&:inspect).join(' ')})"
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # XPath 1.0 implementation for Moxml
5
+ #
6
+ # This module provides a complete XPath 1.0 engine for querying XML
7
+ # documents, particularly for the Ox adapter which has limited native
8
+ # XPath support.
9
+ #
10
+ # @example Basic usage
11
+ # engine = Moxml::XPath::Engine.new(document)
12
+ # results = engine.evaluate("//book[@id='123']")
13
+ #
14
+ module XPath
15
+ autoload :Engine, "moxml/xpath/engine"
16
+ autoload :Context, "moxml/xpath/context"
17
+ autoload :Conversion, "moxml/xpath/conversion"
18
+ autoload :Cache, "moxml/xpath/cache"
19
+ autoload :Lexer, "moxml/xpath/lexer"
20
+ autoload :Parser, "moxml/xpath/parser"
21
+ autoload :Compiler, "moxml/xpath/compiler"
22
+
23
+ # Require errors directly so classes are immediately available
24
+ require_relative "xpath/errors"
25
+
26
+ # AST nodes for expression representation
27
+ module AST
28
+ autoload :Node, "moxml/xpath/ast/node"
29
+ end
30
+
31
+ # Ruby AST generation for compiling XPath
32
+ module Ruby
33
+ autoload :Node, "moxml/xpath/ruby/node"
34
+ autoload :Generator, "moxml/xpath/ruby/generator"
35
+ end
36
+ end
37
+ end
data/lib/moxml.rb CHANGED
@@ -4,7 +4,7 @@ module Moxml
4
4
  class << self
5
5
  def new(adapter = nil, &block)
6
6
  context = Context.new(adapter)
7
- context.config.instance_eval(&block) if block_given?
7
+ context.config.instance_eval(&block) if block
8
8
  context
9
9
  end
10
10
 
@@ -12,7 +12,8 @@ module Moxml
12
12
  yield Config.default if block_given?
13
13
  end
14
14
 
15
- def with_config(adapter_name = nil, strict_parsing = nil, default_encoding = nil)
15
+ def with_config(adapter_name = nil, strict_parsing = nil,
16
+ default_encoding = nil)
16
17
  original_config = Config.default.dup
17
18
 
18
19
  configure do |config|
@@ -42,3 +43,5 @@ require_relative "moxml/builder"
42
43
  require_relative "moxml/config"
43
44
  require_relative "moxml/context"
44
45
  require_relative "moxml/adapter"
46
+ require_relative "moxml/xpath"
47
+ require_relative "moxml/sax"
data/moxml.gemspec CHANGED
@@ -10,7 +10,9 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Unified XML manipulation library"
12
12
  spec.description = <<~DESCRIPTION
13
- Moxml is a unified XML manipulation library that provides a common API.
13
+ Moxml is a unified XML manipulation library that provides a common API
14
+ for XML node navigation, manipulation, building and XPath querying
15
+ interface covering multiple parser implementations including Nokogiri, Oga, REXML, Ox, LibXML.
14
16
  DESCRIPTION
15
17
 
16
18
  spec.homepage = "https://github.com/lutaml/moxml"
@@ -0,0 +1,6 @@
1
+ # CustomizedLibxml wrapper classes are tested via:
2
+ # spec/moxml/adapter/libxml_spec.rb
3
+ #
4
+ # Individual wrapper spec files removed to avoid redundant pending tests.
5
+ # The wrapper classes (Element, Text, Cdata, Comment, etc.) are thoroughly
6
+ # tested through the main LibXML adapter test suite.
@@ -0,0 +1,77 @@
1
+ # Consistency Tests
2
+
3
+ ## Purpose
4
+
5
+ This directory contains cross-adapter consistency tests that verify all adapters produce equivalent results for the same operations. These tests act as a quality gate to ensure adapter parity.
6
+
7
+ ## What Should Be Placed Here
8
+
9
+ - ✅ Tests verifying all adapters produce equivalent XML output
10
+ - ✅ Tests ensuring API parity across adapters
11
+ - ✅ Tests catching adapter-specific quirks
12
+ - ✅ Tests for serialization consistency
13
+ - ✅ Tests for parsing equivalence
14
+
15
+ ## What Should NOT Be Placed Here
16
+
17
+ - ❌ Adapter-specific implementation tests (use adapter/ instead)
18
+ - ❌ Unit tests (use unit/ instead)
19
+ - ❌ Performance benchmarks (use performance/ instead)
20
+ - ❌ Documentation examples (use examples/ instead)
21
+
22
+ ## How to Run
23
+
24
+ ```bash
25
+ # Run all consistency tests
26
+ bundle exec rake spec:consistency
27
+
28
+ # Run specific consistency test
29
+ bundle exec rspec spec/consistency/adapter_parity_spec.rb
30
+ ```
31
+
32
+ ## Directory Structure
33
+
34
+ ```
35
+ consistency/
36
+ └── adapter_parity_spec.rb # Ensures all adapters produce equivalent results
37
+ ```
38
+
39
+ ## Writing Consistency Tests
40
+
41
+ Consistency tests should compare output across all adapters:
42
+
43
+ ```ruby
44
+ RSpec.describe "Adapter Parity" do
45
+ describe "Serialization consistency" do
46
+ it "produces equivalent XML across adapters" do
47
+ results = {}
48
+
49
+ Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter|
50
+ Moxml.with_config(adapter) do
51
+ doc = Moxml.parse("<root><child>text</child></root>")
52
+ results[adapter] = doc.to_xml
53
+ end
54
+ end
55
+
56
+ # All results should be equivalent (allowing for formatting differences)
57
+ expect(results.values.uniq.length).to eq(1)
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## Purpose
64
+
65
+ These tests ensure that:
66
+ 1. Users can switch adapters without changing behavior
67
+ 2. All adapters implement the same API surface
68
+ 3. No adapter-specific bugs leak into user code
69
+ 4. Documentation examples work with all adapters
70
+
71
+ ## CI Integration
72
+
73
+ Consistency tests should run:
74
+ - On every pull request
75
+ - Before releases
76
+ - As a quality gate
77
+ - To catch regressions
@@ -18,10 +18,10 @@ RSpec.describe "Adapter Examples" do
18
18
 
19
19
  def normalize_xml(xml)
20
20
  xml.gsub(/>\s+</, "><")
21
- .gsub(/\s+/, " ")
22
- .gsub(/ >/, ">")
23
- .gsub(/\?></, "?>\n<")
24
- .strip
21
+ .gsub(/\s+/, " ")
22
+ .gsub(" >", ">")
23
+ .gsub("?><", "?>\n<")
24
+ .strip
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,75 @@
1
+ # Example Tests
2
+
3
+ ## Purpose
4
+
5
+ This directory contains executable documentation examples. These tests verify that code examples from documentation and README work correctly and serve as living documentation.
6
+
7
+ ## What Should Be Placed Here
8
+
9
+ - ✅ Tests for documentation examples
10
+ - ✅ Tests for README code snippets
11
+ - ✅ Examples that demonstrate API usage
12
+ - ✅ Tutorial-style example tests
13
+ - ✅ Tests that can be extracted for documentation
14
+
15
+ ## What Should NOT Be Placed Here
16
+
17
+ - ❌ Comprehensive test coverage (use unit/ or integration/ instead)
18
+ - ❌ Edge case testing (use integration/ instead)
19
+ - ❌ Performance benchmarks (use performance/ instead)
20
+ - ❌ Adapter-specific tests (use adapter/ instead)
21
+
22
+ ## How to Run
23
+
24
+ ```bash
25
+ # Run all example tests
26
+ bundle exec rake spec:examples
27
+
28
+ # Run specific example test
29
+ bundle exec rspec spec/examples/basic_usage_spec.rb
30
+
31
+ # Examples can be skipped in CI
32
+ bundle exec rspec --tag ~examples
33
+ ```
34
+
35
+ ## Directory Structure
36
+
37
+ ```
38
+ examples/
39
+ ├── basic_usage_spec.rb # README basic usage examples
40
+ ├── namespace_examples_spec.rb # Namespace handling examples
41
+ ├── xpath_examples_spec.rb # XPath query examples
42
+ └── attribute_examples_spec.rb # Attribute manipulation examples
43
+ ```
44
+
45
+ ## Writing Example Tests
46
+
47
+ Example tests should:
48
+ 1. Be simple and easy to understand
49
+ 2. Demonstrate real-world usage patterns
50
+ 3. Run with all adapters to ensure portability
51
+ 4. Be suitable for extraction into documentation
52
+
53
+ ```ruby
54
+ RSpec.describe "Basic Usage Examples" do
55
+ Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter_name|
56
+ context "with #{adapter_name} adapter" do
57
+ let(:context) { Moxml.new(adapter_name) }
58
+
59
+ it "parses XML" do
60
+ doc = context.parse("<root>Text</root>")
61
+ expect(doc.root.text).to eq("Text")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ ```
67
+
68
+ ## Tagging
69
+
70
+ Example tests are tagged with `:examples` and can be excluded:
71
+
72
+ ```ruby
73
+ RSpec.describe "Example", :examples do
74
+ # ...
75
+ end