moxml 0.1.26 → 0.2.0
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/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +21 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
module CustomizedLeptris
|
|
6
|
+
# A text run split out of a native text node that carried entity
|
|
7
|
+
# markers. Value object: the moxml contract exposes text nodes as
|
|
8
|
+
# separate children around entity references, while libleptris
|
|
9
|
+
# stores them as one marker-bearing text node.
|
|
10
|
+
class TextSegment
|
|
11
|
+
attr_accessor :parent
|
|
12
|
+
|
|
13
|
+
attr_reader :content
|
|
14
|
+
|
|
15
|
+
def initialize(content, parent = nil)
|
|
16
|
+
@content = content
|
|
17
|
+
@parent = parent
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.is_a?(self.class) && content == other.content
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
# Pure-Ruby node kinds that libleptris has no native representation
|
|
6
|
+
# for: XML declarations, programmatic DOCTYPEs, and entity references.
|
|
7
|
+
# The adapter stores them via NativeAttachment and serializes them
|
|
8
|
+
# from #to_xml.
|
|
9
|
+
module CustomizedLeptris
|
|
10
|
+
autoload :Declaration, "moxml/adapter/customized_leptris/declaration"
|
|
11
|
+
autoload :Doctype, "moxml/adapter/customized_leptris/doctype"
|
|
12
|
+
autoload :EntityReference, "moxml/adapter/customized_leptris/entity_reference"
|
|
13
|
+
autoload :TextSegment, "moxml/adapter/customized_leptris/text_segment"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -5,19 +5,10 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML CDATA section nodes
|
|
7
7
|
class Cdata < Node
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# libxml stores CDATA payload verbatim. Only the `]]>` end-marker
|
|
9
|
+
# needs splitting before re-wrapping.
|
|
10
10
|
def to_xml
|
|
11
|
-
|
|
12
|
-
.gsub(""", '"')
|
|
13
|
-
.gsub("'", "'")
|
|
14
|
-
.gsub("<", "<")
|
|
15
|
-
.gsub(">", ">")
|
|
16
|
-
.gsub("&", "&")
|
|
17
|
-
|
|
18
|
-
# Handle CDATA end marker escaping (]]> becomes ]]]]><![CDATA[>)
|
|
19
|
-
# Replace all ]]> markers in the content before wrapping
|
|
20
|
-
escaped_content = content.gsub("]]>", "]]]]><![CDATA[>")
|
|
11
|
+
escaped_content = @native.content.gsub("]]>", "]]]]><![CDATA[>")
|
|
21
12
|
"<![CDATA[#{escaped_content}]]>"
|
|
22
13
|
end
|
|
23
14
|
end
|
|
@@ -5,16 +5,9 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML comment nodes
|
|
7
7
|
class Comment < Node
|
|
8
|
-
#
|
|
9
|
-
# LibXML auto-escapes content, we need to un-escape it
|
|
8
|
+
# libxml stores comment payload verbatim.
|
|
10
9
|
def to_xml
|
|
11
|
-
|
|
12
|
-
.gsub(""", '"')
|
|
13
|
-
.gsub("'", "'")
|
|
14
|
-
.gsub("<", "<")
|
|
15
|
-
.gsub(">", ">")
|
|
16
|
-
.gsub("&", "&")
|
|
17
|
-
"<!--#{content}-->"
|
|
10
|
+
"<!--#{@native.content}-->"
|
|
18
11
|
end
|
|
19
12
|
end
|
|
20
13
|
end
|
|
@@ -47,16 +47,7 @@ module Moxml
|
|
|
47
47
|
|
|
48
48
|
# Generate XML declaration string
|
|
49
49
|
def to_xml
|
|
50
|
-
|
|
51
|
-
if @encoding && !@encoding.empty?
|
|
52
|
-
output << " encoding=\"#{@encoding}\""
|
|
53
|
-
end
|
|
54
|
-
# Include standalone attribute if explicitly set (true or false)
|
|
55
|
-
unless @standalone_value.nil?
|
|
56
|
-
output << " standalone=\"#{standalone}\""
|
|
57
|
-
end
|
|
58
|
-
output << "?>"
|
|
59
|
-
output
|
|
50
|
+
XmlEmitter.declaration_xml(@version, @encoding, standalone)
|
|
60
51
|
end
|
|
61
52
|
|
|
62
53
|
private
|
|
@@ -18,6 +18,14 @@ module Moxml
|
|
|
18
18
|
@native = native_node
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Swap the wrapped native node. Used by the Libxml adapter when
|
|
22
|
+
# libxml-ruby's content= setter would silently re-escape stored
|
|
23
|
+
# text; replacing the node with a fresh raw-storage instance is
|
|
24
|
+
# the only way to preserve verbatim content.
|
|
25
|
+
def replace_native!(fresh)
|
|
26
|
+
@native = fresh
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
# Compare wrappers based on their native nodes
|
|
22
30
|
def ==(other)
|
|
23
31
|
return false unless other
|
|
@@ -5,20 +5,12 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML processing instruction nodes
|
|
7
7
|
class ProcessingInstruction < Node
|
|
8
|
-
#
|
|
9
|
-
# LibXML auto-escapes content, we need to un-escape it
|
|
8
|
+
# XML 1.0 §2.6: PI content is verbatim — no entity resolution, no escaping.
|
|
10
9
|
def to_xml
|
|
11
10
|
target = @native.name
|
|
12
11
|
content = @native.content
|
|
13
|
-
|
|
14
|
-
# Un-escape LibXML's automatic escaping
|
|
15
12
|
if content && !content.empty?
|
|
16
|
-
|
|
17
|
-
.gsub("'", "'")
|
|
18
|
-
.gsub("<", "<")
|
|
19
|
-
.gsub(">", ">")
|
|
20
|
-
.gsub("&", "&")
|
|
21
|
-
"<?#{target} #{unescaped}?>"
|
|
13
|
+
"<?#{target} #{content}?>"
|
|
22
14
|
else
|
|
23
15
|
"<?#{target}?>"
|
|
24
16
|
end
|
|
@@ -13,16 +13,9 @@ module Moxml
|
|
|
13
13
|
@native.content
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# LibXML's .content already contains escaped text, but it over-escapes
|
|
18
|
-
# quotes which don't need escaping in text nodes (only in attributes)
|
|
16
|
+
# @native.to_s escapes & < > but leaves quotes alone, which text nodes need.
|
|
19
17
|
def to_xml
|
|
20
|
-
|
|
21
|
-
# Skip the gsub allocation entirely when there's nothing to undo —
|
|
22
|
-
# the common case for parsed text without literal quotes.
|
|
23
|
-
return content unless content.include?(""")
|
|
24
|
-
|
|
25
|
-
content.gsub(""", '"')
|
|
18
|
+
@native.to_s
|
|
26
19
|
end
|
|
27
20
|
end
|
|
28
21
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "oga"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Adapter
|
|
7
|
+
module CustomizedOga
|
|
8
|
+
# Override Oga::EntityDecoder.decode for XML inputs.
|
|
9
|
+
#
|
|
10
|
+
# Oga 3.4's stock Oga::XML::Entities.decode runs multiple passes,
|
|
11
|
+
# turning the well-formed "&#38;" into "&" rather than the
|
|
12
|
+
# spec-correct "&". XML 1.0 §4.6 forbids recursive resolution
|
|
13
|
+
# of parsed entities — exactly one decode pass is correct.
|
|
14
|
+
#
|
|
15
|
+
# Moxml::Adapter::Base.decode_entities does the single pass. HTML
|
|
16
|
+
# inputs fall through to Oga's stock HTML::Entities.decode, which
|
|
17
|
+
# has HTML-specific legacy rules that differ from XML's.
|
|
18
|
+
#
|
|
19
|
+
# Prepending on the singleton class means every Oga reader
|
|
20
|
+
# (Text#text, Attribute#value, etc.) automatically picks up the
|
|
21
|
+
# fixed decoder, so the adapter no longer needs to walk the parsed
|
|
22
|
+
# tree rewriting @text/@value ivars behind Oga's back.
|
|
23
|
+
module EntityDecoderOverride
|
|
24
|
+
# rubocop:disable-next Style/OptionalBooleanParameter -- must match Oga's signature
|
|
25
|
+
def decode(input, html = false)
|
|
26
|
+
return super if html
|
|
27
|
+
|
|
28
|
+
::Moxml::Adapter::Base.decode_entities(input)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Oga::EntityDecoder.singleton_class.prepend(
|
|
36
|
+
Moxml::Adapter::CustomizedOga::EntityDecoderOverride,
|
|
37
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "oga"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Adapter
|
|
7
|
+
module CustomizedOga
|
|
8
|
+
# Bypass Oga's lazy decoder for user-authored values.
|
|
9
|
+
#
|
|
10
|
+
# Oga::XML::Attribute#value and Oga::XML::Text#text decode `@value` /
|
|
11
|
+
# `@text` on first read via Oga::EntityDecoder. EntityDecoderOverride
|
|
12
|
+
# makes that decode single-pass for parsed input. But user-authored
|
|
13
|
+
# values are already data — running them through the decoder again
|
|
14
|
+
# would re-interpret any literal `&` / `&#NN;` substrings as entity
|
|
15
|
+
# references. So we route the read through the NativeAttachment
|
|
16
|
+
# sidecar first; if the adapter stored the verbatim value there, we
|
|
17
|
+
# return it as-is, otherwise we fall through to Oga's normal read.
|
|
18
|
+
module RawValueOverride
|
|
19
|
+
def value
|
|
20
|
+
cached = ::Moxml::Adapter::Oga.attachments.get(self, :raw_value)
|
|
21
|
+
return cached unless cached.nil?
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def value=(new_value)
|
|
27
|
+
::Moxml::Adapter::Oga.attachments.set(self, :raw_value, new_value)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def text
|
|
32
|
+
cached = ::Moxml::Adapter::Oga.attachments.get(self, :raw_text)
|
|
33
|
+
return cached unless cached.nil?
|
|
34
|
+
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def text=(new_value)
|
|
39
|
+
::Moxml::Adapter::Oga.attachments.set(self, :raw_text, new_value)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Oga::XML::Attribute.prepend(
|
|
48
|
+
Moxml::Adapter::CustomizedOga::RawValueOverride,
|
|
49
|
+
)
|
|
50
|
+
Oga::XML::Text.prepend(
|
|
51
|
+
Moxml::Adapter::CustomizedOga::RawValueOverride,
|
|
52
|
+
)
|
|
@@ -45,36 +45,10 @@ module Moxml
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def on_cdata(node, output)
|
|
48
|
-
#
|
|
48
|
+
# Split any embedded end sequence into adjacent sections
|
|
49
49
|
return super unless node.text.include?("]]>")
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
chunks = ["]]", ">"] if chunks.size == 1
|
|
53
|
-
|
|
54
|
-
while (index = chunks.index("]]>"))
|
|
55
|
-
# the end tag cannot be the first and the last at the same time
|
|
56
|
-
|
|
57
|
-
if index.zero?
|
|
58
|
-
# it's the first text chunk
|
|
59
|
-
chunks[index] = "]]"
|
|
60
|
-
chunks[index + 1] = ">#{chunks[index + 1]}"
|
|
61
|
-
elsif index - 1 == chunks.size
|
|
62
|
-
# it's the last text chunk
|
|
63
|
-
chunks[index - 1] += "]]"
|
|
64
|
-
chunks[index] = ">"
|
|
65
|
-
else
|
|
66
|
-
# it's a chunk in the middle
|
|
67
|
-
chunks[index - 1] += "]]"
|
|
68
|
-
chunks[index + 1] = ">#{chunks[index + 1]}"
|
|
69
|
-
chunks.delete_at(index)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
chunks.each do |chunk|
|
|
74
|
-
output << "<![CDATA[#{chunk}]]>"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
output
|
|
51
|
+
output << ::Moxml::XmlEmitter.cdata(node.text)
|
|
78
52
|
end
|
|
79
53
|
|
|
80
54
|
def on_processing_instruction(node, output)
|
|
@@ -91,12 +65,8 @@ module Moxml
|
|
|
91
65
|
protected
|
|
92
66
|
|
|
93
67
|
def encode(input)
|
|
94
|
-
#
|
|
95
|
-
input
|
|
96
|
-
::Oga::XML::Entities::ENCODE_ATTRIBUTE_REGEXP,
|
|
97
|
-
# Keep apostrophes in attributes
|
|
98
|
-
::Oga::XML::Entities::ENCODE_ATTRIBUTE_MAPPING.merge("'" => "'"),
|
|
99
|
-
)
|
|
68
|
+
# moxml-canonical attribute form: apostrophes stay literal
|
|
69
|
+
::Moxml::XmlEmitter.escape_attribute(input.to_s)
|
|
100
70
|
end
|
|
101
71
|
end
|
|
102
72
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
module Adapter
|
|
5
5
|
module CustomizedOga
|
|
6
|
+
autoload :EntityDecoderOverride, "moxml/adapter/customized_oga/entity_decoder"
|
|
7
|
+
autoload :RawValueOverride, "moxml/adapter/customized_oga/raw_value_override"
|
|
6
8
|
autoload :XmlDeclaration, "moxml/adapter/customized_oga/xml_declaration"
|
|
7
9
|
autoload :XmlGenerator, "moxml/adapter/customized_oga/xml_generator"
|
|
8
10
|
end
|
|
@@ -3,23 +3,7 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
module Adapter
|
|
5
5
|
module CustomizedOx
|
|
6
|
-
|
|
7
|
-
attr_reader :name
|
|
8
|
-
attr_accessor :parent
|
|
9
|
-
|
|
10
|
-
def initialize(name)
|
|
11
|
-
@name = name
|
|
12
|
-
@parent = nil
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def to_xml
|
|
16
|
-
"&#{@name};"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def ==(other)
|
|
20
|
-
other.is_a?(self.class) && @name == other.name
|
|
21
|
-
end
|
|
22
|
-
end
|
|
6
|
+
EntityReference = ::Moxml::Entity::Reference
|
|
23
7
|
end
|
|
24
8
|
end
|
|
25
9
|
end
|
|
@@ -3,17 +3,7 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
module Adapter
|
|
5
5
|
module CustomizedRexml
|
|
6
|
-
|
|
7
|
-
attr_reader :name
|
|
8
|
-
|
|
9
|
-
def initialize(name)
|
|
10
|
-
@name = name
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def ==(other)
|
|
14
|
-
other.is_a?(self.class) && @name == other.name
|
|
15
|
-
end
|
|
16
|
-
end
|
|
6
|
+
EntityReference = ::Moxml::Entity::Reference
|
|
17
7
|
end
|
|
18
8
|
end
|
|
19
9
|
end
|
|
@@ -6,15 +6,13 @@ require "moxml/adapter/ox"
|
|
|
6
6
|
|
|
7
7
|
module Moxml
|
|
8
8
|
module Adapter
|
|
9
|
-
# HeadedOx adapter -
|
|
9
|
+
# HeadedOx adapter - Ox parsing with Moxml's XPath engine.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# Ox's locate() method, HeadedOx provides full XPath 1.0 functionality
|
|
17
|
-
# including all axes, predicates, and 27 standard functions.
|
|
11
|
+
# Since the Ox adapter now routes XPath through Moxml's pure-Ruby
|
|
12
|
+
# XPath 1.0 engine, HeadedOx is the historical name for exactly
|
|
13
|
+
# that combination and inherits everything from Adapter::Ox. It
|
|
14
|
+
# exists for backwards compatibility and reports its own
|
|
15
|
+
# capabilities.
|
|
18
16
|
#
|
|
19
17
|
# @example
|
|
20
18
|
# context = Moxml.new(:headed_ox)
|
|
@@ -23,138 +21,23 @@ module Moxml
|
|
|
23
21
|
#
|
|
24
22
|
class HeadedOx < Ox
|
|
25
23
|
class << self
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# ~176K allocations per 100-element parse). Lazy parse defers wrapper
|
|
29
|
-
# creation until nodes are accessed, matching Ox adapter behavior.
|
|
30
|
-
def parse(xml, options = {}, _context = nil)
|
|
31
|
-
processed_xml = preprocess_entities(xml)
|
|
32
|
-
native_doc = begin
|
|
33
|
-
result = ::Ox.parse(processed_xml)
|
|
34
|
-
|
|
35
|
-
# result can be either Document or Element
|
|
36
|
-
if result.is_a?(::Ox::Document)
|
|
37
|
-
assign_parents(result)
|
|
38
|
-
validate_single_root(result) if options[:strict]
|
|
39
|
-
result
|
|
40
|
-
else
|
|
41
|
-
doc = ::Ox::Document.new
|
|
42
|
-
doc << result
|
|
43
|
-
assign_parents(doc)
|
|
44
|
-
doc
|
|
45
|
-
end
|
|
46
|
-
rescue ::Ox::ParseError => e
|
|
47
|
-
raise Moxml::ParseError.new(
|
|
48
|
-
e.message,
|
|
49
|
-
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Use provided context if available, otherwise create new one
|
|
54
|
-
ctx = _context || Context.new(:headed_ox)
|
|
55
|
-
Document.new(native_doc, ctx)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Execute XPath query using Moxml's XPath engine
|
|
59
|
-
#
|
|
60
|
-
# This overrides the Ox adapter's xpath method which uses locate().
|
|
61
|
-
#
|
|
62
|
-
# @param node Starting node (native or wrapped)
|
|
63
|
-
# @param [String] expression XPath expression
|
|
64
|
-
# @param [Hash] namespaces Namespace prefix mappings
|
|
65
|
-
# @return [Array, Object] Native node array or scalar value
|
|
66
|
-
def xpath(node, expression, namespaces = {})
|
|
67
|
-
unless node.is_a?(Moxml::Node)
|
|
68
|
-
ctx = Context.new(:headed_ox)
|
|
69
|
-
node = Moxml::Node.wrap(node, ctx)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Parse XPath expression to AST
|
|
73
|
-
ast = XPath::Parser.parse(expression)
|
|
74
|
-
|
|
75
|
-
# Compile AST to executable Proc using class method
|
|
76
|
-
proc = XPath::Compiler.compile_with_cache(ast, namespaces: namespaces)
|
|
77
|
-
|
|
78
|
-
# Execute on the node (now guaranteed to be wrapped Moxml node)
|
|
79
|
-
result = proc.call(node)
|
|
80
|
-
|
|
81
|
-
# Return native arrays for Node#xpath to wrap, scalars directly.
|
|
82
|
-
# The adapter contract: xpath() returns Array<native> | scalar.
|
|
83
|
-
case result
|
|
84
|
-
when Array
|
|
85
|
-
# XPath engine returns wrapped Moxml::Node objects.
|
|
86
|
-
# Extract native nodes and deduplicate by object identity.
|
|
87
|
-
native_nodes = result.map { |n| n.is_a?(Moxml::Node) ? n.native : n }
|
|
88
|
-
seen = {}
|
|
89
|
-
native_nodes.select do |native|
|
|
90
|
-
id = native.object_id
|
|
91
|
-
if seen[id]
|
|
92
|
-
false
|
|
93
|
-
else
|
|
94
|
-
seen[id] = true
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
when NodeSet
|
|
98
|
-
# NodeSet from intermediate evaluation - extract natives and deduplicate
|
|
99
|
-
seen = {}
|
|
100
|
-
result.to_a.map(&:native).select do |native|
|
|
101
|
-
id = native.object_id
|
|
102
|
-
if seen[id]
|
|
103
|
-
false
|
|
104
|
-
else
|
|
105
|
-
seen[id] = true
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
else
|
|
109
|
-
# Scalar values (string, number, boolean) - return as-is
|
|
110
|
-
result
|
|
111
|
-
end
|
|
112
|
-
rescue StandardError => e
|
|
113
|
-
raise Moxml::XPathError.new(
|
|
114
|
-
"XPath execution failed: #{e.message}",
|
|
115
|
-
expression: expression,
|
|
116
|
-
adapter: "HeadedOx",
|
|
117
|
-
node: node,
|
|
118
|
-
)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Execute XPath query and return first result
|
|
122
|
-
#
|
|
123
|
-
# @param [Moxml::Node] node Starting node
|
|
124
|
-
# @param [String] expression XPath expression
|
|
125
|
-
# @param [Hash] namespaces Namespace prefix mappings
|
|
126
|
-
# @return [Object, nil] First native node or scalar value
|
|
127
|
-
def at_xpath(node, expression, namespaces = {})
|
|
128
|
-
result = xpath(node, expression, namespaces)
|
|
129
|
-
result.is_a?(Array) ? result.first : result
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Check if XPath is supported
|
|
133
|
-
#
|
|
134
|
-
# @return [Boolean] Always true for HeadedOx
|
|
135
|
-
def xpath_supported?
|
|
136
|
-
true
|
|
24
|
+
def context_adapter_name
|
|
25
|
+
:headed_ox
|
|
137
26
|
end
|
|
138
27
|
|
|
139
28
|
# Report adapter capabilities
|
|
140
29
|
#
|
|
141
|
-
# HeadedOx extends Ox's capabilities with full XPath support
|
|
142
|
-
# through Moxml's XPath engine
|
|
143
|
-
#
|
|
144
30
|
# @return [Hash] Capability flags
|
|
145
31
|
def capabilities
|
|
146
32
|
{
|
|
147
|
-
# Core adapter capabilities
|
|
148
33
|
parse: true,
|
|
149
|
-
|
|
150
|
-
# Parsing capabilities (inherited from Ox)
|
|
151
34
|
sax_parsing: true,
|
|
152
35
|
namespace_aware: true,
|
|
153
36
|
namespace_support: :partial,
|
|
154
37
|
dtd_support: true,
|
|
155
38
|
parsing_speed: :fast,
|
|
156
39
|
|
|
157
|
-
# XPath
|
|
40
|
+
# XPath via Moxml's XPath engine
|
|
158
41
|
xpath_support: :full,
|
|
159
42
|
xpath_full: true,
|
|
160
43
|
xpath_axes: :partial, # 6 of 13 axes: child, descendant, descendant-or-self, self, attribute, parent
|
|
@@ -163,11 +46,9 @@ module Moxml
|
|
|
163
46
|
xpath_namespaces: true,
|
|
164
47
|
xpath_variables: true,
|
|
165
48
|
|
|
166
|
-
# Serialization capabilities (inherited from Ox)
|
|
167
49
|
namespace_serialization: true,
|
|
168
50
|
pretty_print: true,
|
|
169
51
|
|
|
170
|
-
# Known limitations
|
|
171
52
|
schema_validation: false,
|
|
172
53
|
xslt_support: false,
|
|
173
54
|
}
|