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
@@ -11,6 +11,24 @@ module Moxml
11
11
 
12
12
  def build(native_doc)
13
13
  @current_doc = context.create_document(native_doc)
14
+
15
+ # Transfer has_declaration flag if present
16
+ if native_doc.respond_to?(:instance_variable_get) &&
17
+ native_doc.instance_variable_defined?(:@moxml_has_declaration)
18
+ has_declaration = native_doc.instance_variable_get(:@moxml_has_declaration)
19
+ @current_doc.has_xml_declaration = has_declaration
20
+ end
21
+
22
+ # Transfer DOCTYPE from parsed document if it exists
23
+ if native_doc.respond_to?(:instance_variable_get) &&
24
+ native_doc.instance_variable_defined?(:@moxml_doctype)
25
+ doctype = native_doc.instance_variable_get(:@moxml_doctype)
26
+ if doctype
27
+ @current_doc.native.instance_variable_set(:@moxml_doctype,
28
+ doctype)
29
+ end
30
+ end
31
+
14
32
  visit_node(native_doc)
15
33
  @current_doc
16
34
  end
@@ -33,6 +51,11 @@ module Moxml
33
51
  def visit_element(node)
34
52
  childless_node = adapter.duplicate_node(node)
35
53
  adapter.replace_children(childless_node, [])
54
+ # Prepare node for new document (LibXML needs this)
55
+ childless_node = adapter.prepare_for_new_document(
56
+ childless_node,
57
+ @current_doc.native,
58
+ )
36
59
  element = Element.new(childless_node, context)
37
60
  @node_stack.last.add_child(element)
38
61
 
@@ -42,23 +65,29 @@ module Moxml
42
65
  end
43
66
 
44
67
  def visit_text(node)
45
- @node_stack.last&.add_child(Text.new(node, context))
68
+ # Prepare node for new document before wrapping
69
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
70
+ @node_stack.last&.add_child(Text.new(prepared, context))
46
71
  end
47
72
 
48
73
  def visit_cdata(node)
49
- @node_stack.last&.add_child(Cdata.new(node, context))
74
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
75
+ @node_stack.last&.add_child(Cdata.new(prepared, context))
50
76
  end
51
77
 
52
78
  def visit_comment(node)
53
- @node_stack.last&.add_child(Comment.new(node, context))
79
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
80
+ @node_stack.last&.add_child(Comment.new(prepared, context))
54
81
  end
55
82
 
56
83
  def visit_processing_instruction(node)
57
- @node_stack.last&.add_child(ProcessingInstruction.new(node, context))
84
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
85
+ @node_stack.last&.add_child(ProcessingInstruction.new(prepared, context))
58
86
  end
59
87
 
60
88
  def visit_doctype(node)
61
- @node_stack.last&.add_child(Doctype.new(node, context))
89
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
90
+ @node_stack.last&.add_child(Doctype.new(prepared, context))
62
91
  end
63
92
 
64
93
  def visit_children(node)
data/lib/moxml/element.rb CHANGED
@@ -13,6 +13,27 @@ module Moxml
13
13
  adapter.set_node_name(@native, value)
14
14
  end
15
15
 
16
+ # Returns the expanded name including namespace prefix
17
+ def expanded_name
18
+ if namespace_prefix && !namespace_prefix.empty?
19
+ "#{namespace_prefix}:#{name}"
20
+ else
21
+ name
22
+ end
23
+ end
24
+
25
+ # Returns the namespace prefix of this element
26
+ def namespace_prefix
27
+ ns = namespace
28
+ ns&.prefix
29
+ end
30
+
31
+ # Returns the namespace URI of this element
32
+ def namespace_uri
33
+ ns = namespace
34
+ ns&.uri
35
+ end
36
+
16
37
  def []=(name, value)
17
38
  adapter.set_attribute(@native, name, normalize_xml_value(value))
18
39
  end
@@ -26,6 +47,16 @@ module Moxml
26
47
  native_attr && Attribute.new(native_attr, context)
27
48
  end
28
49
 
50
+ # Alias for attribute access
51
+ def get(attr_name)
52
+ attribute(attr_name)
53
+ end
54
+
55
+ # Alias for getting attribute value (used by XPath engine)
56
+ def get(attr_name)
57
+ self[attr_name]
58
+ end
59
+
29
60
  def attributes
30
61
  adapter.attributes(@native).map do |attr|
31
62
  Attribute.new(attr, context)
@@ -42,7 +73,14 @@ module Moxml
42
73
  adapter.create_native_namespace(@native, prefix, uri)
43
74
  self
44
75
  rescue ValidationError => e
45
- raise Moxml::NamespaceError, e.message
76
+ # Re-raise as NamespaceError, provide attributes for error context
77
+ # but the to_s will only add details if provided
78
+ raise Moxml::NamespaceError.new(
79
+ e.message,
80
+ prefix: prefix,
81
+ uri: uri,
82
+ element: self,
83
+ )
46
84
  end
47
85
  alias add_namespace_definition add_namespace
48
86
 
@@ -58,7 +96,7 @@ module Moxml
58
96
  if ns_or_hash.is_a?(Hash)
59
97
  adapter.set_namespace(
60
98
  @native,
61
- adapter.create_namespace(@native, *ns_or_hash.to_a.first)
99
+ adapter.create_namespace(@native, *ns_or_hash.to_a.first),
62
100
  )
63
101
  else
64
102
  adapter.set_namespace(@native, ns_or_hash&.native)
@@ -72,6 +110,11 @@ module Moxml
72
110
  end
73
111
  alias namespace_definitions namespaces
74
112
 
113
+ # Returns the namespace URI of this element (alias for namespace_uri)
114
+ def namespace_name
115
+ namespace_uri
116
+ end
117
+
75
118
  def text
76
119
  adapter.text_content(@native)
77
120
  end
@@ -108,5 +151,31 @@ module Moxml
108
151
  self.text = content
109
152
  self
110
153
  end
154
+
155
+ # Bulk attribute setting
156
+ def set_attributes(attributes_hash)
157
+ attributes_hash.each { |name, value| self[name] = value }
158
+ self
159
+ end
160
+
161
+ # Chainable child addition
162
+ def with_child(child)
163
+ add_child(child)
164
+ self
165
+ end
166
+
167
+ # Convenience find methods
168
+ def find_element(xpath)
169
+ at_xpath(xpath)
170
+ end
171
+
172
+ def find_all(xpath)
173
+ xpath(xpath).to_a
174
+ end
175
+
176
+ # Alias for children (used by XPath engine)
177
+ def nodes
178
+ children
179
+ end
111
180
  end
112
181
  end
data/lib/moxml/error.rb CHANGED
@@ -1,20 +1,189 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
+ # Base error class for all Moxml errors
4
5
  class Error < StandardError; end
5
6
 
7
+ # Error raised when parsing XML fails
6
8
  class ParseError < Error
7
- attr_reader :line, :column
9
+ attr_reader :line, :column, :source
8
10
 
9
- def initialize(message, line: nil, column: nil)
11
+ def initialize(message, line: nil, column: nil, source: nil)
10
12
  @line = line
11
13
  @column = column
14
+ @source = source
12
15
  super(message)
13
16
  end
17
+
18
+ def to_s
19
+ msg = super
20
+ msg += "\n Line: #{@line}" if @line
21
+ msg += "\n Column: #{@column}" if @column
22
+ msg += "\n Source: #{@source.inspect}" if @source
23
+ msg += "\n Hint: Check XML syntax and ensure all tags are properly closed"
24
+ msg
25
+ end
26
+ end
27
+
28
+ # Error raised when XPath expression evaluation fails
29
+ class XPathError < Error
30
+ attr_reader :expression, :adapter, :node
31
+
32
+ def initialize(message, expression: nil, adapter: nil, node: nil)
33
+ @expression = expression
34
+ @adapter = adapter
35
+ @node = node
36
+ super(message)
37
+ end
38
+
39
+ def to_s
40
+ msg = super
41
+ msg += "\n Expression: #{@expression}" if @expression
42
+ msg += "\n Adapter: #{@adapter}" if @adapter
43
+ msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
44
+ msg += "\n Hint: Verify XPath syntax and ensure the adapter supports the expression"
45
+ msg
46
+ end
47
+ end
48
+
49
+ # Error raised when XML validation fails
50
+ class ValidationError < Error
51
+ attr_reader :node, :constraint, :value
52
+
53
+ def initialize(message, node: nil, constraint: nil, value: nil)
54
+ @node = node
55
+ @constraint = constraint
56
+ @value = value
57
+ super(message)
58
+ end
59
+
60
+ def to_s
61
+ msg = super
62
+ # Only add extra details if any were provided
63
+ has_details = @node.respond_to?(:name) || @constraint || @value
64
+ if has_details
65
+ msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
66
+ msg += "\n Constraint: #{@constraint}" if @constraint
67
+ msg += "\n Value: #{@value.inspect}" if @value
68
+ msg += "\n Hint: Ensure the value meets XML specification requirements"
69
+ end
70
+ msg
71
+ end
72
+ end
73
+
74
+ # Error raised when namespace operations fail
75
+ class NamespaceError < Error
76
+ attr_reader :prefix, :uri, :element
77
+
78
+ def initialize(message, prefix: nil, uri: nil, element: nil)
79
+ @prefix = prefix
80
+ @uri = uri
81
+ @element = element
82
+ super(message)
83
+ end
84
+ end
85
+
86
+ # Error raised when adapter operations fail
87
+ class AdapterError < Error
88
+ attr_reader :adapter_name, :operation, :native_error
89
+
90
+ def initialize(message, adapter: nil, operation: nil, native_error: nil)
91
+ @adapter_name = adapter
92
+ @operation = operation
93
+ @native_error = native_error
94
+ super(message)
95
+ end
96
+
97
+ def to_s
98
+ msg = super
99
+ msg += "\n Adapter: #{@adapter_name}" if @adapter_name
100
+ msg += "\n Operation: #{@operation}" if @operation
101
+ if @native_error
102
+ msg += "\n Native Error: #{@native_error.class.name}: #{@native_error.message}"
103
+ end
104
+ msg += "\n Hint: Ensure the adapter gem is properly installed and compatible"
105
+ msg
106
+ end
14
107
  end
15
108
 
16
- class ValidationError < Error; end
17
- class XPathError < Error; end
18
- class NamespaceError < Error; end
19
- class AdapterError < Error; end
109
+ # Error raised when serialization fails
110
+ class SerializationError < Error
111
+ attr_reader :node, :adapter, :format
112
+
113
+ def initialize(message, node: nil, adapter: nil, format: nil)
114
+ @node = node
115
+ @adapter = adapter
116
+ @format = format
117
+ super(message)
118
+ end
119
+
120
+ def to_s
121
+ msg = super
122
+ msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
123
+ msg += "\n Adapter: #{@adapter}" if @adapter
124
+ msg += "\n Format: #{@format}" if @format
125
+ msg += "\n Hint: Check that the node structure is valid for serialization"
126
+ msg
127
+ end
128
+ end
129
+
130
+ # Error raised when document structure is invalid
131
+ class DocumentStructureError < Error
132
+ attr_reader :attempted_operation, :current_state
133
+
134
+ def initialize(message, operation: nil, state: nil)
135
+ @attempted_operation = operation
136
+ @current_state = state
137
+ super(message)
138
+ end
139
+
140
+ def to_s
141
+ msg = super
142
+ msg += "\n Operation: #{@attempted_operation}" if @attempted_operation
143
+ msg += "\n Current State: #{@current_state}" if @current_state
144
+ msg += "\n Hint: Ensure the document structure follows XML specifications"
145
+ msg
146
+ end
147
+ end
148
+
149
+ # Error raised when attribute operations fail
150
+ class AttributeError < Error
151
+ attr_reader :attribute_name, :element, :value
152
+
153
+ def initialize(message, name: nil, element: nil, value: nil)
154
+ @attribute_name = name
155
+ @element = element
156
+ @value = value
157
+ super(message)
158
+ end
159
+
160
+ def to_s
161
+ msg = super
162
+ msg += "\n Attribute: #{@attribute_name}" if @attribute_name
163
+ msg += "\n Element: <#{@element.name}>" if @element.respond_to?(:name)
164
+ msg += "\n Value: #{@value.inspect}" if @value
165
+ msg += "\n Hint: Verify attribute name follows XML naming rules"
166
+ msg
167
+ end
168
+ end
169
+
170
+ # Error raised when a feature is not implemented by an adapter
171
+ class NotImplementedError < Error
172
+ attr_reader :feature, :adapter
173
+
174
+ def initialize(message = nil, feature: nil, adapter: nil)
175
+ @feature = feature
176
+ @adapter = adapter
177
+ message ||= "Feature not implemented"
178
+ super(message)
179
+ end
180
+
181
+ def to_s
182
+ msg = super
183
+ msg += "\n Feature: #{@feature}" if @feature
184
+ msg += "\n Adapter: #{@adapter}" if @adapter
185
+ msg += "\n Hint: This feature may not be supported by the current adapter"
186
+ msg
187
+ end
188
+ end
20
189
  end
data/lib/moxml/node.rb CHANGED
@@ -31,7 +31,7 @@ module Moxml
31
31
  def children
32
32
  NodeSet.new(
33
33
  adapter.children(@native).map { adapter.patch_node(_1, @native) },
34
- context
34
+ context,
35
35
  )
36
36
  end
37
37
 
@@ -73,7 +73,13 @@ module Moxml
73
73
  end
74
74
 
75
75
  def to_xml(options = {})
76
- adapter.serialize(@native, default_options.merge(options))
76
+ # Determine if we should include XML declaration
77
+ # For Document nodes: check native then wrapper, unless explicitly overridden
78
+ # For other nodes: default to no declaration unless explicitly set
79
+ serialize_options = default_options.merge(options)
80
+ serialize_options[:no_declaration] = !should_include_declaration?(options)
81
+
82
+ adapter.serialize(@native, serialize_options)
77
83
  end
78
84
 
79
85
  def xpath(expression, namespaces = {})
@@ -84,6 +90,92 @@ module Moxml
84
90
  Node.wrap(adapter.at_xpath(@native, expression, namespaces), context)
85
91
  end
86
92
 
93
+ # Convenience find methods (aliases for xpath methods)
94
+ def find(xpath_expression, namespaces = {})
95
+ at_xpath(xpath_expression, namespaces)
96
+ end
97
+
98
+ def find_all(xpath_expression, namespaces = {})
99
+ xpath(xpath_expression, namespaces).to_a
100
+ end
101
+
102
+ # Check if node has any children
103
+ def has_children?
104
+ !children.empty?
105
+ end
106
+
107
+ # Get first/last child
108
+ def first_child
109
+ children.first
110
+ end
111
+
112
+ def last_child
113
+ children.last
114
+ end
115
+
116
+ # Returns the text content of this node
117
+ # For elements, returns concatenated text of all text children
118
+ # For text nodes, returns the content if available
119
+ def text
120
+ if respond_to?(:content)
121
+ content
122
+ elsif respond_to?(:children)
123
+ children.select { |c| c.is_a?(Text) }.map(&:content).join
124
+ else
125
+ ""
126
+ end
127
+ end
128
+
129
+ # Returns the text content of this node
130
+ # Subclasses should override this method
131
+ # Element and Text have their own implementations
132
+ def text
133
+ ""
134
+ end
135
+
136
+ # Attribute accessor - only works on Element nodes
137
+ # Returns nil for non-element nodes
138
+ def [](name)
139
+ return nil unless respond_to?(:attribute)
140
+
141
+ attr = attribute(name)
142
+ attr&.value if attr.respond_to?(:value)
143
+ end
144
+
145
+ # Returns the namespace of this node
146
+ # Only applicable to Element nodes, returns nil for others
147
+ def namespace
148
+ return nil unless element?
149
+
150
+ ns = adapter.namespace(@native)
151
+ ns && Namespace.new(ns, context)
152
+ end
153
+
154
+ # Returns all namespace definitions on this node
155
+ # Only applicable to Element nodes, returns empty array for others
156
+ def namespaces
157
+ return [] unless element?
158
+
159
+ adapter.namespace_definitions(@native).map do |ns|
160
+ Namespace.new(ns, context)
161
+ end
162
+ end
163
+
164
+ # Recursively yield all descendant nodes
165
+ # Used by XPath descendant-or-self and descendant axes
166
+ def each_node(&block)
167
+ children.each do |child|
168
+ yield child
169
+ child.each_node(&block) if child.respond_to?(:each_node)
170
+ end
171
+ end
172
+
173
+ # Clone the node (deep copy)
174
+ def clone
175
+ Node.wrap(adapter.dup(@native), context)
176
+ end
177
+ alias dup clone
178
+
87
179
  def ==(other)
88
180
  self.class == other.class && @native == other.native
89
181
  end
@@ -106,6 +198,7 @@ module Moxml
106
198
  when :document then Document
107
199
  when :declaration then Declaration
108
200
  when :doctype then Doctype
201
+ when :attribute then Attribute
109
202
  else self
110
203
  end
111
204
 
@@ -129,7 +222,11 @@ module Moxml
129
222
  when String then Text.new(adapter.create_text(node), context)
130
223
  when Node then node
131
224
  else
132
- raise ArgumentError, "Invalid node type: #{node.class}"
225
+ raise Moxml::DocumentStructureError.new(
226
+ "Invalid node type: #{node.class}. Expected String or Moxml::Node",
227
+ operation: "prepare_node",
228
+ state: "node_type: #{node.class}",
229
+ )
133
230
  end
134
231
  end
135
232
 
@@ -141,8 +238,62 @@ module Moxml
141
238
  # Oga: <empty /> (with a space)
142
239
  # Nokogiri: <empty/> (without a space)
143
240
  # The expanded format is enforced to avoid this conflict
144
- expand_empty: true
241
+ expand_empty: true,
145
242
  }
146
243
  end
244
+
245
+ def should_include_declaration?(options)
246
+ return options[:declaration] if options.key?(:declaration)
247
+ return options.fetch(:declaration, false) unless is_a?(Document)
248
+
249
+ # For Document nodes, check both wrapper flag and native state
250
+ # Wrapper flag is set by Context.parse for parsed documents
251
+ # Native state reflects programmatic changes (e.g., add/remove)
252
+
253
+ adapter_name = adapter.to_s.split("::").last
254
+
255
+ case adapter_name
256
+ when "Nokogiri"
257
+ # Nokogiri: if @xml_decl is explicitly set, use that state
258
+ # Otherwise, trust wrapper flag (for parsed documents)
259
+ if native.respond_to?(:instance_variable_defined?) &&
260
+ native.instance_variable_defined?(:@xml_decl)
261
+ # Explicitly set (programmatically added) - check if nil
262
+ !native.instance_variable_get(:@xml_decl).nil?
263
+ else
264
+ # Not set (parsed document) - trust wrapper flag
265
+ has_xml_declaration
266
+ end
267
+ when "Rexml"
268
+ # REXML: check @xml_declaration instance variable
269
+ # If not defined (parsed doc), trust wrapper flag
270
+ if native.respond_to?(:instance_variable_defined?) &&
271
+ native.instance_variable_defined?(:@xml_declaration)
272
+ # Explicitly set - check if nil
273
+ !native.instance_variable_get(:@xml_declaration).nil?
274
+ else
275
+ # Not set (parsed document) - trust wrapper flag
276
+ has_xml_declaration
277
+ end
278
+ when "Oga"
279
+ native.respond_to?(:xml_declaration) && !native.xml_declaration.nil?
280
+ when "Ox", "HeadedOx"
281
+ # Ox stores declaration in document attributes
282
+ native[:version] || native[:encoding] || native[:standalone]
283
+ when "Libxml"
284
+ # LibXML stores declaration wrapper as instance variable
285
+ if native.respond_to?(:instance_variable_defined?) &&
286
+ native.instance_variable_defined?(:@moxml_declaration)
287
+ # Explicitly set - check if nil
288
+ !native.instance_variable_get(:@moxml_declaration).nil?
289
+ else
290
+ # Not set - trust wrapper flag
291
+ has_xml_declaration
292
+ end
293
+ else
294
+ # Fallback - trust wrapper flag
295
+ has_xml_declaration
296
+ end
297
+ end
147
298
  end
148
299
  end
@@ -52,6 +52,31 @@ module Moxml
52
52
  self.class.new(nodes + other.nodes, context)
53
53
  end
54
54
 
55
+ def <<(node)
56
+ # If it's a wrapped Moxml node, unwrap to native before storing
57
+ native_node = node.respond_to?(:native) ? node.native : node
58
+ @nodes << native_node
59
+ self
60
+ end
61
+ alias push <<
62
+
63
+ # Deduplicate nodes based on native object identity
64
+ # This is crucial for XPath operations like descendant-or-self
65
+ # which may yield the same native node multiple times
66
+ def uniq_by_native
67
+ seen = {}
68
+ unique_natives = @nodes.select do |native|
69
+ id = native.object_id
70
+ if seen[id]
71
+ false
72
+ else
73
+ seen[id] = true
74
+ true
75
+ end
76
+ end
77
+ self.class.new(unique_natives, context)
78
+ end
79
+
55
80
  def ==(other)
56
81
  self.class == other.class &&
57
82
  length == other.length &&
@@ -68,5 +93,14 @@ module Moxml
68
93
  each(&:remove)
69
94
  self
70
95
  end
96
+
97
+ # Delete a node from the set
98
+ # Accepts both wrapped Moxml nodes and native nodes
99
+ def delete(node)
100
+ # If it's a wrapped Moxml node, unwrap to native
101
+ native_node = node.respond_to?(:native) ? node.native : node
102
+ @nodes.delete(native_node)
103
+ self
104
+ end
71
105
  end
72
106
  end