moxml 0.1.26 → 0.3.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 +6 -1
- data/lib/moxml/adapter/base.rb +53 -56
- 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 +137 -117
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +139 -55
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +64 -51
- 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/builder.rb +0 -18
- data/lib/moxml/config.rb +29 -5
- data/lib/moxml/context.rb +33 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +126 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +135 -10
- data/lib/moxml/node_set.rb +1 -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 +143 -8
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -3
- 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 +18 -1
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- 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 +15 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- 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/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/error_spec.rb +0 -50
- 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 +112 -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 +23 -6
- data/lib/moxml/adapter/libxml/entity_restorer.rb +0 -94
- data/lib/moxml/document_builder.rb +0 -167
- data/spec/moxml/document_builder_spec.rb +0 -30
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Entity
|
|
5
|
+
# A named entity reference (&name;) as a value object, for
|
|
6
|
+
# adapters whose native trees cannot represent entity references
|
|
7
|
+
# (ox stores them in the tree; rexml and leptris carry them
|
|
8
|
+
# alongside). Adapter-specific namespaces alias this class so
|
|
9
|
+
# node_type dispatch keeps working.
|
|
10
|
+
class Reference
|
|
11
|
+
attr_reader :name
|
|
12
|
+
attr_accessor :parent
|
|
13
|
+
|
|
14
|
+
def initialize(name)
|
|
15
|
+
@name = name
|
|
16
|
+
@parent = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_xml
|
|
20
|
+
"&#{name};"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(Reference) && name == other.name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Entity
|
|
5
|
+
# Restores configured character entities into explicit
|
|
6
|
+
# Moxml::Entity::Reference nodes after a native tree has been
|
|
7
|
+
# parsed and wrapped — for adapters whose parser resolves entities
|
|
8
|
+
# during parsing (libxml, and the restore_entities option on
|
|
9
|
+
# rexml/oga).
|
|
10
|
+
class Restorer
|
|
11
|
+
def initialize(doc)
|
|
12
|
+
@doc = doc
|
|
13
|
+
@ctx = doc.context
|
|
14
|
+
@registry = @ctx.entity_registry
|
|
15
|
+
@config = @ctx.config
|
|
16
|
+
@adapter = @ctx.config.adapter
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
return unless @registry && @doc.root
|
|
21
|
+
|
|
22
|
+
walk(@doc.root)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def walk(element)
|
|
28
|
+
# Snapshot because we may add/remove siblings during the walk.
|
|
29
|
+
element.children.to_a.each do |child|
|
|
30
|
+
if child.is_a?(::Moxml::Text)
|
|
31
|
+
restore_text_node(child)
|
|
32
|
+
elsif child.is_a?(::Moxml::Element)
|
|
33
|
+
walk(child)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Includes the adapter limitation that adjacent native text
|
|
39
|
+
# nodes get merged.
|
|
40
|
+
def restore_text_node(text_node)
|
|
41
|
+
content = text_node.content
|
|
42
|
+
return unless content
|
|
43
|
+
|
|
44
|
+
chunks = chunk_text(content)
|
|
45
|
+
return if chunks.size == 1 && chunks.first.first == :text
|
|
46
|
+
|
|
47
|
+
parent = text_node.parent
|
|
48
|
+
return unless parent
|
|
49
|
+
|
|
50
|
+
text_node.remove
|
|
51
|
+
chunks.each { |type, payload| append_chunk(parent, type, payload) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def chunk_text(content)
|
|
55
|
+
chunks = []
|
|
56
|
+
buffer = +""
|
|
57
|
+
restorable = @registry.restorable_codepoints
|
|
58
|
+
|
|
59
|
+
content.each_char do |char|
|
|
60
|
+
cp = char.ord
|
|
61
|
+
if restorable.include?(cp) &&
|
|
62
|
+
(name = @registry.primary_name_for_codepoint(cp)) &&
|
|
63
|
+
@registry.should_restore?(cp, config: @config)
|
|
64
|
+
unless buffer.empty?
|
|
65
|
+
chunks << [:text, buffer.dup]
|
|
66
|
+
buffer.clear
|
|
67
|
+
end
|
|
68
|
+
chunks << [:eref, name]
|
|
69
|
+
else
|
|
70
|
+
buffer << char
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
chunks << [:text, buffer.dup] unless buffer.empty?
|
|
75
|
+
chunks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def append_chunk(parent, type, payload)
|
|
79
|
+
case type
|
|
80
|
+
when :text
|
|
81
|
+
parent.add_child(::Moxml::Text.new(
|
|
82
|
+
@adapter.create_native_text(payload), @ctx
|
|
83
|
+
))
|
|
84
|
+
when :eref
|
|
85
|
+
parent.add_child(
|
|
86
|
+
::Moxml::EntityReference.new(
|
|
87
|
+
@adapter.create_native_entity_reference(payload),
|
|
88
|
+
@ctx,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/moxml/entity.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Entity-reference round-trip pipeline.
|
|
5
|
+
#
|
|
6
|
+
# XML libraries split into two camps: those that preserve named
|
|
7
|
+
# entity references natively (nokogiri) and those that expand or
|
|
8
|
+
# reject them at parse (ox, rexml, oga, libxml, leptris). Moxml
|
|
9
|
+
# bridges the camps with one pipeline, owned here and delegated to
|
|
10
|
+
# by Adapter::Base:
|
|
11
|
+
#
|
|
12
|
+
# 1. Parse time — .preprocess_entities rewrites non-standard
|
|
13
|
+
# entity references to a two-character marker (see MARKER) so
|
|
14
|
+
# the reference survives native parsing verbatim. The five
|
|
15
|
+
# standard entities (& < > " ') are NOT
|
|
16
|
+
# converted.
|
|
17
|
+
# 2. Read time — marker-bearing text is split into text and
|
|
18
|
+
# Entity::Reference nodes (adapters whose natives cannot hold
|
|
19
|
+
# references), or decoded in place (.decode_entities covers the
|
|
20
|
+
# five standard entities plus numeric character references).
|
|
21
|
+
# 3. Serialize time — .restore_entities maps markers back to named
|
|
22
|
+
# references, including markers a native serializer already
|
|
23
|
+
# rendered as character references.
|
|
24
|
+
#
|
|
25
|
+
# How references are STORED between parse and serialize is
|
|
26
|
+
# adapter-shaped (in-tree value objects for ox, attachment
|
|
27
|
+
# sequences for rexml/libxml, text markers for oga/leptris); the
|
|
28
|
+
# marker lifecycle and the Entity::Reference value type are the
|
|
29
|
+
# shared, single-sourced parts.
|
|
30
|
+
module Entity
|
|
31
|
+
autoload :Reference, "moxml/entity/reference"
|
|
32
|
+
autoload :Restorer, "moxml/entity/restorer"
|
|
33
|
+
|
|
34
|
+
# Marker for adapters that resolve entities during parsing.
|
|
35
|
+
# U+FFFC (Object Replacement Character) + U+FEFF (BOM) is a
|
|
36
|
+
# two-character sentinel chosen because this exact sequence
|
|
37
|
+
# followed by a valid entity name pattern is vanishingly unlikely
|
|
38
|
+
# in real XML content.
|
|
39
|
+
MARKER = "\u{FFFC}\u{FEFF}"
|
|
40
|
+
NAME_PATTERN = "[a-zA-Z_][\\w.:-]*"
|
|
41
|
+
NAME_RE = /&(#{NAME_PATTERN});/
|
|
42
|
+
MARKER_RE = /\u{FFFC}\u{FEFF}(#{NAME_PATTERN});/
|
|
43
|
+
SERIALIZED_MARKER_RE = /(#{NAME_PATTERN});/
|
|
44
|
+
STANDARD_ENTITIES = %w[amp lt gt quot apos].freeze
|
|
45
|
+
|
|
46
|
+
NAMED_DECODE_MAP = {
|
|
47
|
+
"amp" => "&", "lt" => "<", "gt" => ">",
|
|
48
|
+
"quot" => '"', "apos" => "'"
|
|
49
|
+
}.freeze
|
|
50
|
+
private_constant :NAMED_DECODE_MAP
|
|
51
|
+
|
|
52
|
+
DECODE_RE = /&(?:(amp|lt|gt|quot|apos)|#(?:(\d+)|[xX]([0-9a-fA-F]+)));/
|
|
53
|
+
private_constant :DECODE_RE
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Replace non-standard entity references with markers before
|
|
58
|
+
# parsing. Always returns a UTF-8 encoded string.
|
|
59
|
+
def preprocess_entities(xml)
|
|
60
|
+
return "" if xml.nil?
|
|
61
|
+
|
|
62
|
+
str = if xml.encoding == Encoding::BINARY
|
|
63
|
+
# Binary strings are assumed to be UTF-8. If the bytes are
|
|
64
|
+
# not valid UTF-8, fall back to encoding as UTF-8 with
|
|
65
|
+
# replacement to avoid raising on gsub.
|
|
66
|
+
dup = xml.dup.force_encoding("UTF-8")
|
|
67
|
+
if dup.valid_encoding?
|
|
68
|
+
dup
|
|
69
|
+
else
|
|
70
|
+
xml.dup.encode("UTF-8",
|
|
71
|
+
"ASCII-8BIT", invalid: :replace, undef: :replace)
|
|
72
|
+
end
|
|
73
|
+
elsif xml.encoding == Encoding::UTF_8
|
|
74
|
+
xml
|
|
75
|
+
else
|
|
76
|
+
xml.encode("UTF-8")
|
|
77
|
+
end
|
|
78
|
+
# Fast path: no `&` means no entity references to mark — skip
|
|
79
|
+
# the regex scan and string allocation entirely. The vast
|
|
80
|
+
# majority of XML payloads contain no entity references.
|
|
81
|
+
return str unless str.include?("&")
|
|
82
|
+
|
|
83
|
+
str.gsub(NAME_RE) do |match|
|
|
84
|
+
STANDARD_ENTITIES.include?(::Regexp.last_match(1)) ? match : "#{MARKER}#{::Regexp.last_match(1)};"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resolve numeric (&#NN; / &#xNN;) and the five standard named
|
|
89
|
+
# (& < > " ') XML entity references in one
|
|
90
|
+
# single pass. The resulting characters are data; no further
|
|
91
|
+
# decoding is applied. Numeric refs producing invalid UTF-8
|
|
92
|
+
# (NUL, surrogate halves, > U+10FFFF) are preserved verbatim
|
|
93
|
+
# to avoid silently emitting malformed bytes.
|
|
94
|
+
def decode_entities(text)
|
|
95
|
+
return text unless text.is_a?(String) && text.include?("&")
|
|
96
|
+
|
|
97
|
+
text.gsub(DECODE_RE) do
|
|
98
|
+
if (named = ::Regexp.last_match(1))
|
|
99
|
+
NAMED_DECODE_MAP[named]
|
|
100
|
+
else
|
|
101
|
+
code = ::Regexp.last_match(2) ? ::Regexp.last_match(2).to_i : ::Regexp.last_match(3).to_i(16)
|
|
102
|
+
if code.zero? || code.between?(0xD800, 0xDFFF) || code > 0x10FFFF
|
|
103
|
+
::Regexp.last_match(0)
|
|
104
|
+
else
|
|
105
|
+
[code].pack("U")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Restore entity markers back to named entity references.
|
|
112
|
+
def restore_entities(text)
|
|
113
|
+
return text unless text.is_a?(String)
|
|
114
|
+
|
|
115
|
+
# Force UTF-8 encoding since markers are UTF-8 characters
|
|
116
|
+
str = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding("UTF-8")
|
|
117
|
+
# Fast path: the vast majority of documents carry no entity
|
|
118
|
+
# markers — two C-level scans beat two regex passes.
|
|
119
|
+
return str unless str.include?(MARKER) ||
|
|
120
|
+
str.include?("")
|
|
121
|
+
|
|
122
|
+
result = str.gsub(MARKER_RE, '&\1;')
|
|
123
|
+
result.gsub(SERIALIZED_MARKER_RE, '&\1;')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -199,7 +199,7 @@ module Moxml
|
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
# Returns the set of codepoints that could potentially be restored as entities.
|
|
202
|
-
# Used by
|
|
202
|
+
# Used by Entity::Restorer for O(1) fast-path checks.
|
|
203
203
|
# @return [Set<Integer>]
|
|
204
204
|
def restorable_codepoints
|
|
205
205
|
@restorable_codepoints ||= if @by_name.empty?
|
data/lib/moxml/error.rb
CHANGED
|
@@ -106,27 +106,6 @@ module Moxml
|
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
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.is_a?(Element) || @node.is_a?(Attribute)
|
|
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
109
|
# Error raised when document structure is invalid
|
|
131
110
|
class DocumentStructureError < Error
|
|
132
111
|
attr_reader :attempted_operation, :current_state
|
|
@@ -146,27 +125,6 @@ module Moxml
|
|
|
146
125
|
end
|
|
147
126
|
end
|
|
148
127
|
|
|
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.is_a?(Element)
|
|
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
128
|
# Error raised when a feature is not implemented by an adapter
|
|
171
129
|
class NotImplementedError < Error
|
|
172
130
|
attr_reader :feature, :adapter
|
data/lib/moxml/node.rb
CHANGED
|
@@ -21,7 +21,12 @@ module Moxml
|
|
|
21
21
|
# Update native reference after identity-changing operations
|
|
22
22
|
# (e.g., LibXML doc.root= creates a new Ruby wrapper)
|
|
23
23
|
def refresh_native!(new_native)
|
|
24
|
-
@native
|
|
24
|
+
unless new_native.equal?(@native)
|
|
25
|
+
context.unregister_wrapper(@native)
|
|
26
|
+
@native = new_native
|
|
27
|
+
context.register_wrapper(new_native, self)
|
|
28
|
+
end
|
|
29
|
+
self
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def document
|
|
@@ -55,6 +60,8 @@ module Moxml
|
|
|
55
60
|
refreshed = adapter.actual_native(node.native, @native)
|
|
56
61
|
node.refresh_native!(refreshed) if refreshed && refreshed != node.native
|
|
57
62
|
node.parent_node = self
|
|
63
|
+
# The adopted subtree's in-scope namespaces changed
|
|
64
|
+
node.invalidate_namespace_cache!
|
|
58
65
|
invalidate_children_cache!
|
|
59
66
|
self
|
|
60
67
|
end
|
|
@@ -77,9 +84,15 @@ module Moxml
|
|
|
77
84
|
invalidate_parent_children_cache!
|
|
78
85
|
adapter.remove(@native)
|
|
79
86
|
invalidate_children_cache!
|
|
87
|
+
# The detached subtree left its declaring ancestors behind
|
|
88
|
+
invalidate_namespace_cache!
|
|
80
89
|
self
|
|
81
90
|
end
|
|
82
91
|
|
|
92
|
+
# Namespace-scope caches live on Element; the base no-op lets tree
|
|
93
|
+
# mutations invalidate uniformly without type checks.
|
|
94
|
+
def invalidate_namespace_cache!; end
|
|
95
|
+
|
|
83
96
|
def replace(node)
|
|
84
97
|
node = prepare_node(node)
|
|
85
98
|
invalidate_parent_children_cache!
|
|
@@ -103,7 +116,10 @@ module Moxml
|
|
|
103
116
|
end
|
|
104
117
|
|
|
105
118
|
def xpath(expression, namespaces = {})
|
|
106
|
-
|
|
119
|
+
result = adapter.xpath(@native, expression, namespaces)
|
|
120
|
+
# Adapter contract: Array<native> | scalar. Scalars (count(),
|
|
121
|
+
# string-length(), booleans) pass through unwrapped.
|
|
122
|
+
result.is_a?(Array) ? NodeSet.new(result, context) : result
|
|
107
123
|
end
|
|
108
124
|
|
|
109
125
|
def at_xpath(expression, namespaces = {})
|
|
@@ -186,6 +202,104 @@ module Moxml
|
|
|
186
202
|
children.each(&block)
|
|
187
203
|
end
|
|
188
204
|
|
|
205
|
+
# Returns all ancestor nodes from the parent up to and including
|
|
206
|
+
# the document node.
|
|
207
|
+
#
|
|
208
|
+
# @return [NodeSet] ancestors ordered nearest-first
|
|
209
|
+
def ancestors
|
|
210
|
+
return NodeSet.new([], context) if document?
|
|
211
|
+
|
|
212
|
+
natives = []
|
|
213
|
+
current = parent
|
|
214
|
+
while current
|
|
215
|
+
natives << current.native
|
|
216
|
+
break if current.document?
|
|
217
|
+
|
|
218
|
+
current = current.parent
|
|
219
|
+
end
|
|
220
|
+
NodeSet.new(natives, context)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns all descendant nodes (children, grandchildren, and so on),
|
|
224
|
+
# excluding the node itself.
|
|
225
|
+
#
|
|
226
|
+
# @return [NodeSet] descendants in document order
|
|
227
|
+
def descendants
|
|
228
|
+
natives = []
|
|
229
|
+
each_node { |node| natives << node.native }
|
|
230
|
+
NodeSet.new(natives, context)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Returns the siblings after this node, in document order.
|
|
234
|
+
#
|
|
235
|
+
# @return [NodeSet]
|
|
236
|
+
def following_siblings
|
|
237
|
+
parent = self.parent
|
|
238
|
+
return NodeSet.new([], context) unless parent
|
|
239
|
+
|
|
240
|
+
siblings = parent.children.to_a
|
|
241
|
+
index = siblings.index { |child| child.native.equal?(@native) }
|
|
242
|
+
return NodeSet.new([], context) if index.nil?
|
|
243
|
+
|
|
244
|
+
NodeSet.new(siblings[(index + 1)..].map(&:native), context)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Returns the siblings before this node, in document order.
|
|
248
|
+
#
|
|
249
|
+
# @return [NodeSet]
|
|
250
|
+
def preceding_siblings
|
|
251
|
+
parent = self.parent
|
|
252
|
+
return NodeSet.new([], context) unless parent
|
|
253
|
+
|
|
254
|
+
siblings = parent.children.to_a
|
|
255
|
+
index = siblings.index { |child| child.native.equal?(@native) }
|
|
256
|
+
return NodeSet.new([], context) if index.nil?
|
|
257
|
+
|
|
258
|
+
NodeSet.new(siblings[0...index].map(&:native), context)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Deep copy of the node (both dup and clone create deep copies for XML nodes)
|
|
262
|
+
def dup
|
|
263
|
+
Moxml::Node.wrap(adapter.duplicate_node(@native), context)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
alias clone dup
|
|
267
|
+
|
|
268
|
+
# Returns an XPath expression that uniquely locates this node within
|
|
269
|
+
# its document. Positional predicates are emitted only when sibling
|
|
270
|
+
# elements share the same qualified name, keeping paths minimal.
|
|
271
|
+
#
|
|
272
|
+
# @return [String] XPath expression
|
|
273
|
+
# @raise [Moxml::NotImplementedError] for node types other than
|
|
274
|
+
# element and document
|
|
275
|
+
def path
|
|
276
|
+
return "/" if document?
|
|
277
|
+
|
|
278
|
+
unless element?
|
|
279
|
+
raise Moxml::NotImplementedError.new(
|
|
280
|
+
"path is only supported for element and document nodes",
|
|
281
|
+
feature: "path",
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
segments = []
|
|
286
|
+
current = self
|
|
287
|
+
while current && !current.document?
|
|
288
|
+
segments.unshift(path_segment_for(current))
|
|
289
|
+
current = current.parent
|
|
290
|
+
end
|
|
291
|
+
"/#{segments.join('/')}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Returns the 1-based line number where this node appears in the
|
|
295
|
+
# source XML, or nil when the underlying adapter does not track
|
|
296
|
+
# source positions.
|
|
297
|
+
#
|
|
298
|
+
# @return [Integer, nil]
|
|
299
|
+
def line_number
|
|
300
|
+
adapter.line_number(@native)
|
|
301
|
+
end
|
|
302
|
+
|
|
189
303
|
def outer_xml
|
|
190
304
|
to_xml
|
|
191
305
|
end
|
|
@@ -202,13 +316,6 @@ module Moxml
|
|
|
202
316
|
text.strip.empty?
|
|
203
317
|
end
|
|
204
318
|
|
|
205
|
-
# Deep copy of the node (both dup and clone create deep copies for XML nodes)
|
|
206
|
-
def dup
|
|
207
|
-
Moxml::Node.wrap(adapter.dup(@native), context)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
alias clone dup
|
|
211
|
-
|
|
212
319
|
def ==(other)
|
|
213
320
|
self.class == other.class && @native == other.native
|
|
214
321
|
end
|
|
@@ -251,10 +358,13 @@ module Moxml
|
|
|
251
358
|
def self.wrap(node, context)
|
|
252
359
|
return nil if node.nil?
|
|
253
360
|
|
|
361
|
+
cached = context.wrapper_for(node)
|
|
362
|
+
return cached if cached
|
|
363
|
+
|
|
254
364
|
type = adapter(context).node_type(node)
|
|
255
365
|
klass = node_type_map[type] || self
|
|
256
366
|
|
|
257
|
-
klass.new(node, context)
|
|
367
|
+
klass.new(node, context).tap { |wrapper| context.register_wrapper(node, wrapper) }
|
|
258
368
|
end
|
|
259
369
|
|
|
260
370
|
# Internal: Set the parent node for cache invalidation tracking.
|
|
@@ -286,6 +396,21 @@ module Moxml
|
|
|
286
396
|
|
|
287
397
|
private
|
|
288
398
|
|
|
399
|
+
# XPath segment for an element: the qualified name, plus a positional
|
|
400
|
+
# predicate only when same-named element siblings make it ambiguous.
|
|
401
|
+
def path_segment_for(element)
|
|
402
|
+
name = element.name
|
|
403
|
+
parent = element.parent
|
|
404
|
+
return name unless parent
|
|
405
|
+
|
|
406
|
+
same_name = parent.children.select do |child|
|
|
407
|
+
child.element? && child.name == name
|
|
408
|
+
end
|
|
409
|
+
return name if same_name.size == 1
|
|
410
|
+
|
|
411
|
+
"#{name}[#{same_name.find_index(element) + 1}]"
|
|
412
|
+
end
|
|
413
|
+
|
|
289
414
|
def prepare_node(node)
|
|
290
415
|
case node
|
|
291
416
|
when String then Text.new(adapter.create_text(node), context)
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Moxml
|
|
|
9
9
|
# attribute. This module provides a single implementation.
|
|
10
10
|
module NamespaceSplitter
|
|
11
11
|
# @param attributes [Hash, Array<Array>] attributes as a hash or array of pairs
|
|
12
|
+
# @yieldparam value [Object] raw attribute/namespace value
|
|
13
|
+
# @yieldreturn [Object] transformed value to store
|
|
12
14
|
# @return [Array(Hash, Hash)] [regular_attrs, namespaces]
|
|
13
15
|
def split_attributes_and_namespaces(attributes)
|
|
14
16
|
attrs = {}
|
|
@@ -16,11 +18,12 @@ module Moxml
|
|
|
16
18
|
|
|
17
19
|
each_attribute(attributes) do |name, value|
|
|
18
20
|
name_s = name.to_s
|
|
21
|
+
v = block_given? ? yield(value) : value
|
|
19
22
|
if name_s == "xmlns" || name_s.start_with?("xmlns:")
|
|
20
23
|
prefix = name_s == "xmlns" ? nil : name_s.sub("xmlns:", "")
|
|
21
|
-
ns[prefix] =
|
|
24
|
+
ns[prefix] = v
|
|
22
25
|
else
|
|
23
|
-
attrs[name_s] =
|
|
26
|
+
attrs[name_s] = v
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -10,7 +10,7 @@ module Moxml
|
|
|
10
10
|
class DSAKeyValue
|
|
11
11
|
attr_accessor :p, :q, :g, :y, :j, :seed, :pgen_counter
|
|
12
12
|
|
|
13
|
-
# rubocop:disable Naming/MethodParameterName -- P/Q/G/Y/J are W3C spec names
|
|
13
|
+
# rubocop:disable-next Naming/MethodParameterName -- P/Q/G/Y/J are W3C spec names
|
|
14
14
|
def initialize(p:, q:, y:, g: nil, j: nil, seed: nil,
|
|
15
15
|
pgen_counter: nil)
|
|
16
16
|
@p = p
|
|
@@ -21,7 +21,6 @@ module Moxml
|
|
|
21
21
|
@seed = seed
|
|
22
22
|
@pgen_counter = pgen_counter
|
|
23
23
|
end
|
|
24
|
-
# rubocop:enable Naming/MethodParameterName
|
|
25
24
|
end
|
|
26
25
|
end
|
|
27
26
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Single source of XML emission knowledge: character escaping,
|
|
5
|
+
# CDATA end-sequence segmentation, and declaration/DOCTYPE
|
|
6
|
+
# rendering. Adapters keep their tree walks (those are
|
|
7
|
+
# adapter-shaped) but stop re-deriving the wire format:
|
|
8
|
+
#
|
|
9
|
+
# - text escapes & < >; attribute values also escape " —
|
|
10
|
+
# apostrophes stay literal (moxml-canonical form)
|
|
11
|
+
# - a CDATA section's end sequence is split into adjacent sections
|
|
12
|
+
# - the moxml declaration shape is <?xml version="..."
|
|
13
|
+
# encoding="..." standalone="..."?> with only the set attributes
|
|
14
|
+
# - DOCTYPE renders PUBLIC and SYSTEM external identifiers
|
|
15
|
+
module XmlEmitter
|
|
16
|
+
TEXT_ESCAPE_RE = /[<>&]/
|
|
17
|
+
TEXT_ESCAPE_MAP = {
|
|
18
|
+
"<" => "<", ">" => ">", "&" => "&"
|
|
19
|
+
}.freeze
|
|
20
|
+
ATTRIBUTE_ESCAPE_RE = /[<>&"]/
|
|
21
|
+
ATTRIBUTE_ESCAPE_MAP = {
|
|
22
|
+
"<" => "<", ">" => ">", "&" => "&", '"' => """
|
|
23
|
+
}.freeze
|
|
24
|
+
CDATA_END = "]]>"
|
|
25
|
+
CDATA_END_SPLIT = "]]]]><![CDATA[>"
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# @return [String] text with & < > escaped
|
|
30
|
+
def escape_text(text)
|
|
31
|
+
str = text.is_a?(String) ? text : text.to_s
|
|
32
|
+
return str unless str.match?(TEXT_ESCAPE_RE)
|
|
33
|
+
|
|
34
|
+
str.gsub(TEXT_ESCAPE_RE, TEXT_ESCAPE_MAP)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [String] attribute value with & < > " escaped
|
|
38
|
+
def escape_attribute(value)
|
|
39
|
+
str = value.is_a?(String) ? value : value.to_s
|
|
40
|
+
return str unless str.match?(ATTRIBUTE_ESCAPE_RE)
|
|
41
|
+
|
|
42
|
+
str.gsub(ATTRIBUTE_ESCAPE_RE, ATTRIBUTE_ESCAPE_MAP)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] a CDATA section, splitting any embedded end
|
|
46
|
+
# sequence into adjacent sections
|
|
47
|
+
def cdata(content)
|
|
48
|
+
"<![CDATA[#{content.to_s.gsub(CDATA_END, CDATA_END_SPLIT)}]]>"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] an XML declaration with only the set attributes
|
|
52
|
+
def declaration_xml(version, encoding, standalone)
|
|
53
|
+
output = %(<?xml version="#{version}")
|
|
54
|
+
output << %( encoding="#{encoding}") if encoding && !encoding.to_s.empty?
|
|
55
|
+
output << %( standalone="#{standalone}") unless standalone.nil?
|
|
56
|
+
output << "?>"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String] a DOCTYPE with its external identifier
|
|
60
|
+
def doctype_xml(name, public_id, system_id)
|
|
61
|
+
output = "<!DOCTYPE #{name}"
|
|
62
|
+
if public_id && !public_id.to_s.empty?
|
|
63
|
+
output << %( PUBLIC "#{public_id}")
|
|
64
|
+
output << %( "#{system_id}") if system_id
|
|
65
|
+
elsif system_id && !system_id.to_s.empty?
|
|
66
|
+
output << %( SYSTEM "#{system_id}")
|
|
67
|
+
end
|
|
68
|
+
output << ">"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|