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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos.json +5 -0
- data/.github/workflows/dependent-tests.yml +20 -0
- data/.github/workflows/docs.yml +59 -0
- data/.github/workflows/rake.yml +10 -10
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +224 -43
- data/Gemfile +14 -9
- data/LICENSE.md +6 -2
- data/README.adoc +535 -373
- data/Rakefile +53 -0
- data/benchmarks/.gitignore +6 -0
- data/benchmarks/generate_report.rb +550 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +138 -0
- data/docs/_guides/advanced-features.adoc +87 -0
- data/docs/_guides/development-testing.adoc +165 -0
- data/docs/_guides/index.adoc +51 -0
- data/docs/_guides/modifying-xml.adoc +292 -0
- data/docs/_guides/parsing-xml.adoc +230 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_guides/xml-declaration.adoc +450 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +97 -0
- data/docs/_pages/adapters/libxml.adoc +285 -0
- data/docs/_pages/adapters/nokogiri.adoc +251 -0
- data/docs/_pages/adapters/oga.adoc +291 -0
- data/docs/_pages/adapters/ox.adoc +56 -0
- data/docs/_pages/adapters/rexml.adoc +292 -0
- data/docs/_pages/best-practices.adoc +429 -0
- data/docs/_pages/compatibility.adoc +467 -0
- data/docs/_pages/configuration.adoc +250 -0
- data/docs/_pages/error-handling.adoc +349 -0
- data/docs/_pages/headed-ox-limitations.adoc +574 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +140 -0
- data/docs/_pages/node-api-reference.adoc +49 -0
- data/docs/_pages/performance.adoc +35 -0
- data/docs/_pages/quick-start.adoc +243 -0
- data/docs/_pages/thread-safety.adoc +28 -0
- data/docs/_references/document-api.adoc +407 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +267 -0
- data/docs/_tutorials/builder-pattern.adoc +342 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +324 -0
- data/docs/_tutorials/xpath-queries.adoc +358 -0
- data/docs/index.adoc +122 -0
- data/examples/README.md +124 -0
- data/examples/api_client/README.md +424 -0
- data/examples/api_client/api_client.rb +394 -0
- data/examples/api_client/example_response.xml +48 -0
- data/examples/headed_ox_example/README.md +90 -0
- data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
- data/examples/rss_parser/README.md +194 -0
- data/examples/rss_parser/example_feed.xml +93 -0
- data/examples/rss_parser/rss_parser.rb +189 -0
- data/examples/sax_parsing/README.md +50 -0
- data/examples/sax_parsing/data_extractor.rb +75 -0
- data/examples/sax_parsing/example.xml +21 -0
- data/examples/sax_parsing/large_file.rb +78 -0
- data/examples/sax_parsing/simple_parser.rb +55 -0
- data/examples/web_scraper/README.md +352 -0
- data/examples/web_scraper/example_page.html +201 -0
- data/examples/web_scraper/web_scraper.rb +312 -0
- data/lib/moxml/adapter/base.rb +107 -28
- data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
- data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
- data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
- data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
- data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
- data/lib/moxml/adapter/customized_rexml/formatter.rb +13 -8
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1564 -0
- data/lib/moxml/adapter/nokogiri.rb +156 -9
- data/lib/moxml/adapter/oga.rb +190 -15
- data/lib/moxml/adapter/ox.rb +322 -28
- data/lib/moxml/adapter/rexml.rb +157 -28
- data/lib/moxml/adapter.rb +21 -4
- data/lib/moxml/attribute.rb +6 -0
- data/lib/moxml/builder.rb +40 -4
- data/lib/moxml/config.rb +8 -3
- data/lib/moxml/context.rb +57 -2
- data/lib/moxml/declaration.rb +9 -0
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +53 -6
- data/lib/moxml/document_builder.rb +34 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +155 -4
- data/lib/moxml/node_set.rb +34 -0
- data/lib/moxml/sax/block_handler.rb +194 -0
- data/lib/moxml/sax/element_handler.rb +124 -0
- data/lib/moxml/sax/handler.rb +113 -0
- data/lib/moxml/sax.rb +31 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +4 -4
- data/lib/moxml/xml_utils.rb +7 -4
- data/lib/moxml/xpath/ast/node.rb +159 -0
- data/lib/moxml/xpath/cache.rb +91 -0
- data/lib/moxml/xpath/compiler.rb +1770 -0
- data/lib/moxml/xpath/context.rb +26 -0
- data/lib/moxml/xpath/conversion.rb +124 -0
- data/lib/moxml/xpath/engine.rb +52 -0
- data/lib/moxml/xpath/errors.rb +101 -0
- data/lib/moxml/xpath/lexer.rb +304 -0
- data/lib/moxml/xpath/parser.rb +485 -0
- data/lib/moxml/xpath/ruby/generator.rb +269 -0
- data/lib/moxml/xpath/ruby/node.rb +193 -0
- data/lib/moxml/xpath.rb +37 -0
- data/lib/moxml.rb +5 -2
- data/moxml.gemspec +3 -1
- data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
- data/spec/consistency/README.md +77 -0
- data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
- data/spec/examples/README.md +75 -0
- data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
- data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
- data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
- data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
- data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
- data/spec/integration/README.md +71 -0
- data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
- data/spec/integration/headed_ox_integration_spec.rb +326 -0
- data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
- data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
- data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
- data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
- data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
- data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
- data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
- data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -5
- data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
- data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
- data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
- data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
- data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
- data/spec/moxml/README.md +41 -0
- data/spec/moxml/adapter/.gitkeep +0 -0
- data/spec/moxml/adapter/README.md +61 -0
- data/spec/moxml/adapter/base_spec.rb +27 -0
- data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
- data/spec/moxml/adapter/libxml_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +9 -8
- data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
- data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
- data/spec/moxml/adapter_spec.rb +16 -0
- data/spec/moxml/attribute_spec.rb +30 -0
- data/spec/moxml/builder_spec.rb +33 -0
- data/spec/moxml/cdata_spec.rb +31 -0
- data/spec/moxml/comment_spec.rb +31 -0
- data/spec/moxml/config_spec.rb +3 -3
- data/spec/moxml/context_spec.rb +28 -0
- data/spec/moxml/declaration_preservation_spec.rb +217 -0
- data/spec/moxml/declaration_spec.rb +36 -0
- data/spec/moxml/doctype_spec.rb +33 -0
- data/spec/moxml/document_builder_spec.rb +30 -0
- data/spec/moxml/document_spec.rb +105 -0
- data/spec/moxml/element_spec.rb +143 -0
- data/spec/moxml/error_spec.rb +266 -22
- data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
- data/spec/moxml/namespace_spec.rb +32 -0
- data/spec/moxml/node_set_spec.rb +39 -0
- data/spec/moxml/node_spec.rb +37 -0
- data/spec/moxml/processing_instruction_spec.rb +34 -0
- data/spec/moxml/sax_spec.rb +1067 -0
- data/spec/moxml/text_spec.rb +31 -0
- data/spec/moxml/version_spec.rb +14 -0
- data/spec/moxml/xml_utils/.gitkeep +0 -0
- data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
- data/spec/moxml/xml_utils_spec.rb +49 -0
- data/spec/moxml/xpath/ast/node_spec.rb +83 -0
- data/spec/moxml/xpath/axes_spec.rb +296 -0
- data/spec/moxml/xpath/cache_spec.rb +358 -0
- data/spec/moxml/xpath/compiler_spec.rb +406 -0
- data/spec/moxml/xpath/context_spec.rb +210 -0
- data/spec/moxml/xpath/conversion_spec.rb +365 -0
- data/spec/moxml/xpath/fixtures/sample.xml +25 -0
- data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
- data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
- data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
- data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
- data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
- data/spec/moxml/xpath/lexer_spec.rb +488 -0
- data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
- data/spec/moxml/xpath/parser_spec.rb +364 -0
- data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
- data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
- data/spec/moxml/xpath_capabilities_spec.rb +199 -0
- data/spec/moxml/xpath_spec.rb +77 -0
- data/spec/performance/README.md +83 -0
- data/spec/performance/benchmark_spec.rb +64 -0
- data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +4 -1
- data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
- data/spec/performance/xpath_benchmark_spec.rb +259 -0
- data/spec/spec_helper.rb +58 -1
- data/spec/support/xml_matchers.rb +1 -1
- metadata +178 -34
- data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
- /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
class
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -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
|