moxml 0.1.6 → 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.
- 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 +12 -4
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +238 -40
- 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 +45 -0
- data/docs/_guides/modifying-xml.adoc +293 -0
- data/docs/_guides/parsing-xml.adoc +231 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +98 -0
- data/docs/_pages/adapters/libxml.adoc +286 -0
- data/docs/_pages/adapters/nokogiri.adoc +252 -0
- data/docs/_pages/adapters/oga.adoc +292 -0
- data/docs/_pages/adapters/ox.adoc +55 -0
- data/docs/_pages/adapters/rexml.adoc +293 -0
- data/docs/_pages/best-practices.adoc +430 -0
- data/docs/_pages/compatibility.adoc +468 -0
- data/docs/_pages/configuration.adoc +251 -0
- data/docs/_pages/error-handling.adoc +350 -0
- data/docs/_pages/headed-ox-limitations.adoc +558 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +141 -0
- data/docs/_pages/node-api-reference.adoc +50 -0
- data/docs/_pages/performance.adoc +36 -0
- data/docs/_pages/quick-start.adoc +244 -0
- data/docs/_pages/thread-safety.adoc +29 -0
- data/docs/_references/document-api.adoc +408 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +268 -0
- data/docs/_tutorials/builder-pattern.adoc +343 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +325 -0
- data/docs/_tutorials/xpath-queries.adoc +359 -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 -3
- data/lib/moxml/adapter/customized_ox/namespace.rb +0 -2
- data/lib/moxml/adapter/customized_ox/text.rb +0 -2
- data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1548 -0
- data/lib/moxml/adapter/nokogiri.rb +121 -9
- data/lib/moxml/adapter/oga.rb +123 -12
- data/lib/moxml/adapter/ox.rb +283 -27
- data/lib/moxml/adapter/rexml.rb +127 -20
- 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 +39 -1
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +39 -6
- data/lib/moxml/document_builder.rb +27 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +94 -3
- 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 +1768 -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 -2
- 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_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} +3 -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 +176 -35
- data/lib/ox/node.rb +0 -9
- 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
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
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
data/lib/moxml/document.rb
CHANGED
|
@@ -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",
|
|
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,
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|