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
data/lib/moxml/adapter.rb CHANGED
@@ -4,14 +4,26 @@ require_relative "adapter/base"
4
4
 
5
5
  module Moxml
6
6
  module Adapter
7
- AVALIABLE_ADAPTERS = %i[nokogiri oga rexml ox].freeze
7
+ AVALIABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
8
8
 
9
9
  class << self
10
10
  def load(name)
11
11
  require_adapter(name)
12
- const_get(name.to_s.capitalize)
12
+ # Handle special case for headed_ox -> HeadedOx
13
+ const_name = case name
14
+ when :headed_ox
15
+ "HeadedOx"
16
+ else
17
+ name.to_s.capitalize
18
+ end
19
+ const_get(const_name)
13
20
  rescue LoadError => e
14
- raise LoadError, "Could not load #{name} adapter. Please ensure the #{name} gem is available: #{e.message}"
21
+ raise Moxml::AdapterError.new(
22
+ "Could not load #{name} adapter. Please ensure the #{name} gem is installed",
23
+ adapter: name,
24
+ operation: "load",
25
+ native_error: e,
26
+ )
15
27
  end
16
28
 
17
29
  private
@@ -23,7 +35,12 @@ module Moxml
23
35
  require name.to_s
24
36
  require "#{__dir__}/adapter/#{name}"
25
37
  rescue LoadError => e
26
- raise LoadError, "Failed to load #{name} adapter: #{e.message}"
38
+ raise Moxml::AdapterError.new(
39
+ "Failed to load #{name} adapter",
40
+ adapter: name,
41
+ operation: "require",
42
+ native_error: e,
43
+ )
27
44
  end
28
45
  end
29
46
  end
@@ -18,6 +18,12 @@ module Moxml
18
18
  adapter.set_attribute_value(@native, new_value)
19
19
  end
20
20
 
21
+ # XPath conversion compatibility - attributes need .text method
22
+ # that returns their value for XPath comparisons
23
+ def text
24
+ value
25
+ end
26
+
21
27
  def namespace
22
28
  ns = adapter.namespace(@native)
23
29
  ns && Namespace.new(ns, context)
data/lib/moxml/builder.rb CHANGED
@@ -15,7 +15,7 @@ module Moxml
15
15
 
16
16
  def declaration(version: "1.0", encoding: "UTF-8", standalone: nil)
17
17
  @current.add_child(
18
- @document.create_declaration(version, encoding, standalone)
18
+ @document.create_declaration(version, encoding, standalone),
19
19
  )
20
20
  end
21
21
 
@@ -23,12 +23,22 @@ module Moxml
23
23
  el = @document.create_element(name)
24
24
 
25
25
  attributes.each do |key, value|
26
- el[key] = value
26
+ if key.to_s == "xmlns"
27
+ # Handle default namespace
28
+ el.add_namespace(nil, value.to_s)
29
+ elsif key.to_s.start_with?("xmlns:")
30
+ # Handle prefixed namespace
31
+ prefix = key.to_s.sub("xmlns:", "")
32
+ el.add_namespace(prefix, value.to_s)
33
+ else
34
+ # Regular attribute
35
+ el[key] = value
36
+ end
27
37
  end
28
38
 
29
39
  @current.add_child(el)
30
40
 
31
- if block_given?
41
+ if block
32
42
  previous = @current
33
43
  @current = el
34
44
  instance_eval(&block)
@@ -52,7 +62,7 @@ module Moxml
52
62
 
53
63
  def processing_instruction(target, content)
54
64
  @current.add_child(
55
- @document.create_processing_instruction(target, content)
65
+ @document.create_processing_instruction(target, content),
56
66
  )
57
67
  end
58
68
 
@@ -60,5 +70,31 @@ module Moxml
60
70
  @current.add_namespace(prefix, uri)
61
71
  @namespaces[prefix] = uri
62
72
  end
73
+
74
+ # Convenience method for DOCTYPE
75
+ def doctype(name, external_id = nil, system_id = nil)
76
+ @current.add_child(
77
+ @document.create_doctype(name, external_id, system_id),
78
+ )
79
+ end
80
+
81
+ # Batch element creation
82
+ def elements(element_specs)
83
+ element_specs.each do |name, content_or_attrs|
84
+ if content_or_attrs.is_a?(Hash)
85
+ element(name, content_or_attrs)
86
+ else
87
+ element(name) { text(content_or_attrs) }
88
+ end
89
+ end
90
+ end
91
+
92
+ # Helper for creating namespaced elements
93
+ def ns_element(namespace_uri, name, attributes = {}, &block)
94
+ el = element(name, attributes, &block)
95
+ prefix = @namespaces.key(namespace_uri)
96
+ el.namespace = { prefix => namespace_uri } if prefix
97
+ el
98
+ end
63
99
  end
64
100
  end
data/lib/moxml/config.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Moxml
4
4
  class Config
5
- VALID_ADAPTERS = %i[nokogiri oga rexml ox].freeze
5
+ VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
6
6
  DEFAULT_ADAPTER = VALID_ADAPTERS.first
7
7
 
8
8
  class << self
@@ -23,7 +23,8 @@ module Moxml
23
23
  :entity_encoding,
24
24
  :default_indent
25
25
 
26
- def initialize(adapter_name = nil, strict_parsing = nil, default_encoding = nil)
26
+ def initialize(adapter_name = nil, strict_parsing = nil,
27
+ default_encoding = nil)
27
28
  self.adapter = adapter_name || Config.default.adapter_name
28
29
  @strict_parsing = strict_parsing || Config.default.strict_parsing
29
30
  @default_encoding = default_encoding || Config.default.default_encoding
@@ -37,7 +38,11 @@ module Moxml
37
38
  @adapter = nil
38
39
 
39
40
  unless VALID_ADAPTERS.include?(name)
40
- raise ArgumentError, "Invalid adapter: #{name}. Valid adapters are: #{VALID_ADAPTERS.join(", ")}"
41
+ raise Moxml::AdapterError.new(
42
+ "Invalid adapter: #{name}",
43
+ adapter: name,
44
+ operation: "set_adapter",
45
+ )
41
46
  end
42
47
 
43
48
  @adapter_name = name
data/lib/moxml/context.rb CHANGED
@@ -16,13 +16,51 @@ module Moxml
16
16
  config.adapter.parse(xml, default_options.merge(options))
17
17
  end
18
18
 
19
+ # Parse XML using SAX (event-driven) parsing
20
+ #
21
+ # SAX parsing is memory-efficient and suitable for large XML files.
22
+ # Provide either a handler object or a block with DSL.
23
+ #
24
+ # @param xml [String, IO] XML string or IO object to parse
25
+ # @param handler [Moxml::SAX::Handler, nil] Handler object (optional if block given)
26
+ # @yield [block] DSL block for defining handlers (optional if handler given)
27
+ # @return [void]
28
+ # @raise [ArgumentError] if neither handler nor block is provided
29
+ #
30
+ # @example With handler object
31
+ # handler = MyHandler.new
32
+ # context.sax_parse(xml_string, handler)
33
+ #
34
+ # @example With block
35
+ # context.sax_parse(xml_string) do
36
+ # start_element { |name, attrs| puts name }
37
+ # characters { |text| puts text }
38
+ # end
39
+ #
40
+ def sax_parse(xml, handler = nil, &block)
41
+ # Load SAX module if not already loaded
42
+ require_relative "sax" unless defined?(Moxml::SAX)
43
+
44
+ # Create block handler if block given
45
+ handler ||= SAX::BlockHandler.new(&block) if block
46
+
47
+ # Validate handler
48
+ raise ArgumentError, "Handler or block required" unless handler
49
+ unless handler.is_a?(SAX::Handler)
50
+ raise ArgumentError, "Handler must inherit from Moxml::SAX::Handler"
51
+ end
52
+
53
+ # Delegate to adapter
54
+ config.adapter.sax_parse(xml, handler)
55
+ end
56
+
19
57
  private
20
58
 
21
59
  def default_options
22
60
  {
23
61
  encoding: config.default_encoding,
24
62
  strict: config.strict_parsing,
25
- indent: config.default_indent
63
+ indent: config.default_indent,
26
64
  }
27
65
  end
28
66
  end
data/lib/moxml/doctype.rb CHANGED
@@ -1,5 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Doctype < Node; end
4
+ class Doctype < Node
5
+ def name
6
+ adapter.doctype_name(@native)
7
+ end
8
+
9
+ def external_id
10
+ adapter.doctype_external_id(@native)
11
+ end
12
+
13
+ def system_id
14
+ adapter.doctype_system_id(@native)
15
+ end
16
+ end
5
17
  end
@@ -40,18 +40,19 @@ module Moxml
40
40
  def create_doctype(name, external_id, system_id)
41
41
  Doctype.new(
42
42
  adapter.create_doctype(name, external_id, system_id),
43
- context
43
+ context,
44
44
  )
45
45
  end
46
46
 
47
47
  def create_processing_instruction(target, content)
48
48
  ProcessingInstruction.new(
49
49
  adapter.create_processing_instruction(target, content),
50
- context
50
+ context,
51
51
  )
52
52
  end
53
53
 
54
- def create_declaration(version = "1.0", encoding = "UTF-8", standalone = nil)
54
+ def create_declaration(version = "1.0", encoding = "UTF-8",
55
+ standalone = nil)
55
56
  decl = adapter.create_declaration(version, encoding, standalone)
56
57
  Declaration.new(decl, context)
57
58
  end
@@ -63,7 +64,8 @@ module Moxml
63
64
  if children.empty?
64
65
  adapter.add_child(@native, node.native)
65
66
  else
66
- adapter.add_previous_sibling(adapter.children(@native).first, node.native)
67
+ adapter.add_previous_sibling(adapter.children(@native).first,
68
+ node.native)
67
69
  end
68
70
  elsif root && !node.is_a?(ProcessingInstruction) && !node.is_a?(Comment)
69
71
  raise Error, "Document already has a root element"
@@ -74,8 +76,21 @@ module Moxml
74
76
  end
75
77
 
76
78
  def xpath(expression, namespaces = nil)
77
- native_nodes = adapter.xpath(@native, expression, namespaces)
78
- NodeSet.new(native_nodes, context)
79
+ result = adapter.xpath(@native, expression, namespaces)
80
+
81
+ # Handle different result types:
82
+ # - Scalar values (from functions): return directly
83
+ # - NodeSet: already wrapped, return directly
84
+ # - Array: wrap in NodeSet
85
+ case result
86
+ when NodeSet, Float, String, TrueClass, FalseClass, NilClass
87
+ result
88
+ when Array
89
+ NodeSet.new(result, context)
90
+ else
91
+ # For other types, try to wrap in NodeSet
92
+ NodeSet.new(result, context)
93
+ end
79
94
  end
80
95
 
81
96
  def at_xpath(expression, namespaces = nil)
@@ -83,5 +98,23 @@ module Moxml
83
98
  Node.wrap(native_node, context)
84
99
  end
85
100
  end
101
+
102
+ # Quick element creation and addition
103
+ def add_element(name, attributes = {}, &block)
104
+ elem = create_element(name)
105
+ attributes.each { |k, v| elem[k] = v }
106
+ add_child(elem)
107
+ block&.call(elem)
108
+ elem
109
+ end
110
+
111
+ # Convenience find methods
112
+ def find(xpath)
113
+ at_xpath(xpath)
114
+ end
115
+
116
+ def find_all(xpath)
117
+ xpath(xpath).to_a
118
+ end
86
119
  end
87
120
  end
@@ -11,6 +11,17 @@ module Moxml
11
11
 
12
12
  def build(native_doc)
13
13
  @current_doc = context.create_document(native_doc)
14
+
15
+ # Transfer DOCTYPE from parsed document if it exists
16
+ if native_doc.respond_to?(:instance_variable_get) &&
17
+ native_doc.instance_variable_defined?(:@moxml_doctype)
18
+ doctype = native_doc.instance_variable_get(:@moxml_doctype)
19
+ if doctype
20
+ @current_doc.native.instance_variable_set(:@moxml_doctype,
21
+ doctype)
22
+ end
23
+ end
24
+
14
25
  visit_node(native_doc)
15
26
  @current_doc
16
27
  end
@@ -33,6 +44,11 @@ module Moxml
33
44
  def visit_element(node)
34
45
  childless_node = adapter.duplicate_node(node)
35
46
  adapter.replace_children(childless_node, [])
47
+ # Prepare node for new document (LibXML needs this)
48
+ childless_node = adapter.prepare_for_new_document(
49
+ childless_node,
50
+ @current_doc.native,
51
+ )
36
52
  element = Element.new(childless_node, context)
37
53
  @node_stack.last.add_child(element)
38
54
 
@@ -42,23 +58,29 @@ module Moxml
42
58
  end
43
59
 
44
60
  def visit_text(node)
45
- @node_stack.last&.add_child(Text.new(node, context))
61
+ # Prepare node for new document before wrapping
62
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
63
+ @node_stack.last&.add_child(Text.new(prepared, context))
46
64
  end
47
65
 
48
66
  def visit_cdata(node)
49
- @node_stack.last&.add_child(Cdata.new(node, context))
67
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
68
+ @node_stack.last&.add_child(Cdata.new(prepared, context))
50
69
  end
51
70
 
52
71
  def visit_comment(node)
53
- @node_stack.last&.add_child(Comment.new(node, context))
72
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
73
+ @node_stack.last&.add_child(Comment.new(prepared, context))
54
74
  end
55
75
 
56
76
  def visit_processing_instruction(node)
57
- @node_stack.last&.add_child(ProcessingInstruction.new(node, context))
77
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
78
+ @node_stack.last&.add_child(ProcessingInstruction.new(prepared, context))
58
79
  end
59
80
 
60
81
  def visit_doctype(node)
61
- @node_stack.last&.add_child(Doctype.new(node, context))
82
+ prepared = adapter.prepare_for_new_document(node, @current_doc.native)
83
+ @node_stack.last&.add_child(Doctype.new(prepared, context))
62
84
  end
63
85
 
64
86
  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