moxml 0.5.30 → 0.5.31
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/docs/_pages/adapter-protocol.adoc +7 -0
- data/docs/_pages/adapters/leptris.adoc +61 -0
- data/docs/_pages/conversion-apis.adoc +18 -0
- data/docs/_pages/performance.adoc +142 -0
- data/lib/compat/opal/moxml_boot.rb +1 -0
- data/lib/moxml/adapter/base.rb +64 -0
- data/lib/moxml/adapter/leptris/markers.rb +4 -1
- data/lib/moxml/adapter/leptris/serialize.rb +34 -17
- data/lib/moxml/adapter/leptris.rb +151 -4
- data/lib/moxml/adapter/libxml.rb +13 -2
- data/lib/moxml/adapter/nokogiri.rb +55 -4
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +8 -0
- data/lib/moxml/attribute_resolver.rb +35 -0
- data/lib/moxml/c14n.rb +30 -0
- data/lib/moxml/context.rb +67 -7
- data/lib/moxml/document.rb +2 -2
- data/lib/moxml/element.rb +50 -5
- data/lib/moxml/lazy_node_set.rb +16 -0
- data/lib/moxml/native_attachment/native.rb +8 -2
- data/lib/moxml/node.rb +33 -22
- data/lib/moxml/node_set.rb +48 -25
- data/lib/moxml/sax/block_handler.rb +8 -4
- data/lib/moxml/sax/namespace_splitter.rb +16 -7
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +12 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/adapter/leptris_spec.rb +216 -1
- data/spec/moxml/adapter/nokogiri_spec.rb +17 -0
- data/spec/moxml/context_spec.rb +13 -0
- data/spec/moxml/element_spec.rb +25 -0
- metadata +3 -2
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -923,12 +923,23 @@ module Moxml
|
|
|
923
923
|
native.name
|
|
924
924
|
end
|
|
925
925
|
|
|
926
|
+
# libxml-ruby 5.x exposes no external/system ID accessor on
|
|
927
|
+
# the DTD node — the DOCTYPE source text is the only source,
|
|
928
|
+
# so re-derive from it (same DOCTYPE_RE as the parse path).
|
|
926
929
|
def doctype_external_id(native)
|
|
927
|
-
native.
|
|
930
|
+
if native.is_a?(DoctypeWrapper)
|
|
931
|
+
native.external_id
|
|
932
|
+
else
|
|
933
|
+
native.to_s[DOCTYPE_RE, 2]
|
|
934
|
+
end
|
|
928
935
|
end
|
|
929
936
|
|
|
930
937
|
def doctype_system_id(native)
|
|
931
|
-
native.
|
|
938
|
+
if native.is_a?(DoctypeWrapper)
|
|
939
|
+
native.system_id
|
|
940
|
+
else
|
|
941
|
+
native.to_s[DOCTYPE_RE, 3] || native.to_s[DOCTYPE_RE, 4]
|
|
942
|
+
end
|
|
932
943
|
end
|
|
933
944
|
|
|
934
945
|
def xpath(node, expression, namespaces = nil)
|
|
@@ -12,12 +12,50 @@ module Moxml
|
|
|
12
12
|
@attachments ||= Moxml::NativeAttachment.new
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def bare_set_qname_safe?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Qualified-name reads are clean on this engine (verified by
|
|
20
|
+
# the expanded-name specs); markers restore in-line when the
|
|
21
|
+
# parse recorded them.
|
|
22
|
+
def bare_get_qname_safe?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fast bare-name read for Element#[]: the native call plus,
|
|
27
|
+
# only when the parse recorded entity markers, their
|
|
28
|
+
# restoration (the resolver path's other real semantic).
|
|
29
|
+
# The marker flag is constant per document — a WeakMap on
|
|
30
|
+
# the adapter beats the per-read document fetch + attachment
|
|
31
|
+
# hash chain.
|
|
32
|
+
def doc_entity_markers?(doc)
|
|
33
|
+
cache = (@doc_markers ||= ObjectSpace::WeakMap.new)
|
|
34
|
+
cached = cache[doc]
|
|
35
|
+
return cached unless cached.nil?
|
|
36
|
+
|
|
37
|
+
cache[doc] = attachments.get(doc, :entity_markers) == true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def bare_attr_value(element, name)
|
|
41
|
+
value = element[name.to_s]
|
|
42
|
+
if value.is_a?(String) && doc_entity_markers?(element.document)
|
|
43
|
+
restore_entities(value)
|
|
44
|
+
else
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def native_identity_stable?
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
15
53
|
def set_root(doc, element)
|
|
16
54
|
doc.root = element
|
|
17
55
|
end
|
|
18
56
|
|
|
19
57
|
def parse(xml, options = {}, _context = nil)
|
|
20
|
-
processed_xml =
|
|
58
|
+
processed_xml, entity_markers = Entity.preprocess_with_marker_flag(xml)
|
|
21
59
|
|
|
22
60
|
# preprocess_entities always returns UTF-8, so tell Nokogiri to
|
|
23
61
|
# parse as UTF-8 regardless of any original encoding option.
|
|
@@ -42,9 +80,20 @@ module Moxml
|
|
|
42
80
|
|
|
43
81
|
# Use provided context if available, otherwise create new one
|
|
44
82
|
ctx = _context || Context.new(:nokogiri)
|
|
83
|
+
attachments.set(native_doc, :entity_markers, entity_markers)
|
|
45
84
|
Document.new(native_doc, ctx)
|
|
46
85
|
end
|
|
47
86
|
|
|
87
|
+
# Tolerant HTML parsing through libxml2's HTML mode: implied
|
|
88
|
+
# html/head/body structure, void elements, lowercased names,
|
|
89
|
+
# the HTML named-entity table. Recovery is inherent —
|
|
90
|
+
# malformed input never raises.
|
|
91
|
+
def parse_html(html, _options = {}, _context = nil)
|
|
92
|
+
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
93
|
+
native_doc = ::Nokogiri::HTML(html_string)
|
|
94
|
+
Document.new(native_doc, _context || Context.new(:nokogiri))
|
|
95
|
+
end
|
|
96
|
+
|
|
48
97
|
# Nokogiri parses with `config.recover` unless strict, so
|
|
49
98
|
# recoverable syntax errors land on `doc.errors` instead of
|
|
50
99
|
# raising (issue #147).
|
|
@@ -386,9 +435,11 @@ module Moxml
|
|
|
386
435
|
|
|
387
436
|
def xpath(node, expression, namespaces = nil)
|
|
388
437
|
result = node.xpath(expression, namespaces)
|
|
389
|
-
# Adapter contract: Array<native> | scalar
|
|
390
|
-
# string-length(), boolean functions return
|
|
391
|
-
|
|
438
|
+
# Adapter contract: Array<native> | LazyNodeSet | scalar
|
|
439
|
+
# (count(), string-length(), boolean functions return
|
|
440
|
+
# scalars). The native node-set passes through lazily —
|
|
441
|
+
# to_a would materialize every result wrapper up front.
|
|
442
|
+
result.is_a?(Enumerable) ? result.extend(Moxml::LazyNodeSet) : result
|
|
392
443
|
rescue ::Nokogiri::XML::XPath::SyntaxError => e
|
|
393
444
|
raise Moxml::XPathError.new(
|
|
394
445
|
e.message,
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -15,6 +15,14 @@ module Moxml
|
|
|
15
15
|
@attachments ||= Moxml::NativeAttachment.new
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def bare_set_qname_safe?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def native_identity_stable?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
def set_root(doc, element)
|
|
19
27
|
existing_root = root(doc)
|
|
20
28
|
element.parent = doc if element.is_a?(::Ox::Node)
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -29,6 +29,27 @@ module Moxml
|
|
|
29
29
|
module_function
|
|
30
30
|
|
|
31
31
|
# @return [Moxml::Attribute, nil] the matching attribute wrapper
|
|
32
|
+
# Value-only prefixed resolution for Element#[]: prefix
|
|
33
|
+
# resolution is the resolver's real work; the match step goes to
|
|
34
|
+
# the engine's expanded-name lookup where the adapter offers it,
|
|
35
|
+
# skipping the attribute-list materialization and per-attr URI
|
|
36
|
+
# probes. Bare names take Element#'s adapter fast path instead.
|
|
37
|
+
def resolve_value(element, name)
|
|
38
|
+
prefix, local = name.split(":", 2)
|
|
39
|
+
return nil if prefix == "xmlns"
|
|
40
|
+
|
|
41
|
+
uri = prefix_uri(element, prefix)
|
|
42
|
+
return nil if uri.nil?
|
|
43
|
+
|
|
44
|
+
adapter = element.context.config.adapter
|
|
45
|
+
if adapter.is_a?(Moxml::Adapter::Leptris)
|
|
46
|
+
return adapter.expanded_attr_value(element.native, uri, local)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr = resolve(element, name)
|
|
50
|
+
attr&.value
|
|
51
|
+
end
|
|
52
|
+
|
|
32
53
|
def resolve(element, name)
|
|
33
54
|
name = name.to_s
|
|
34
55
|
if name.include?(":")
|
|
@@ -91,6 +112,20 @@ module Moxml
|
|
|
91
112
|
return value
|
|
92
113
|
end
|
|
93
114
|
|
|
115
|
+
# Fast path: every adapter's set_attribute writes by qualified
|
|
116
|
+
# name, and XML cannot namespace an unprefixed attribute — so
|
|
117
|
+
# a bare-name set replaces only the no-namespace attribute and
|
|
118
|
+
# can never touch a namespaced p:<local> sibling. The full
|
|
119
|
+
# expanded-name resolve (wrap every attribute, resolve each
|
|
120
|
+
# prefix against in-scope namespaces) is only needed for
|
|
121
|
+
# prefixed names; it dominated programmatic-builder attribute
|
|
122
|
+
# writes.
|
|
123
|
+
if !name.include?(":") && adapter.bare_set_qname_safe?
|
|
124
|
+
adapter.set_attribute(element.native, name, value)
|
|
125
|
+
element.invalidate_attribute_cache!
|
|
126
|
+
return value
|
|
127
|
+
end
|
|
128
|
+
|
|
94
129
|
existing = resolve(element, name)
|
|
95
130
|
if existing
|
|
96
131
|
existing.value = value
|
data/lib/moxml/c14n.rb
CHANGED
|
@@ -39,6 +39,11 @@ module Moxml
|
|
|
39
39
|
# :exclusive10 Exclusive C14N 1.0 (W3C REC-xml-exc-c14n-20020718)
|
|
40
40
|
def self.canonicalize(node_or_xml, with_comments: false,
|
|
41
41
|
algorithm: :inclusive10, inclusive_namespaces: [])
|
|
42
|
+
if (native = native_inclusive10(node_or_xml, with_comments,
|
|
43
|
+
inclusive_namespaces))
|
|
44
|
+
return native
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
engine_for(algorithm).canonicalize(
|
|
43
48
|
node_or_xml,
|
|
44
49
|
with_comments: with_comments,
|
|
@@ -46,6 +51,31 @@ module Moxml
|
|
|
46
51
|
)
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
# The leptris engine canonicalizes in C — 23x the Ruby reference.
|
|
55
|
+
# Delegated only for the exact default shape (Inclusive 1.0, no
|
|
56
|
+
# comments, no InclusiveNamespace prefix list) on wrappers whose
|
|
57
|
+
# adapter passed the NATIVE_C14N_BYTE_SAFE load probe; every
|
|
58
|
+
# other combination keeps the Ruby engine (node-set subsets,
|
|
59
|
+
# xml:base fixup, exclusive, 1.1 stay on the ported
|
|
60
|
+
# implementation regardless).
|
|
61
|
+
def self.native_inclusive10(node_or_xml, with_comments, inclusive_namespaces)
|
|
62
|
+
return nil if with_comments || !inclusive_namespaces.empty?
|
|
63
|
+
return nil unless algorithm_shape_inclusive10?(node_or_xml)
|
|
64
|
+
|
|
65
|
+
adapter = node_or_xml.context.config.adapter
|
|
66
|
+
return nil unless adapter == Moxml::Adapter::Leptris &&
|
|
67
|
+
Moxml::Adapter::Leptris.native_c14n_byte_safe?
|
|
68
|
+
|
|
69
|
+
node_or_xml.native.canonicalize(
|
|
70
|
+
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
71
|
+
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.algorithm_shape_inclusive10?(node_or_xml)
|
|
76
|
+
node_or_xml.is_a?(Moxml::Element) || node_or_xml.is_a?(Moxml::Document)
|
|
77
|
+
end
|
|
78
|
+
|
|
49
79
|
def self.canonicalize_inclusive10(node_or_xml, with_comments: false)
|
|
50
80
|
Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
51
81
|
end
|
data/lib/moxml/context.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Moxml
|
|
4
4
|
class Context
|
|
5
|
+
# Opal's default runtime excludes ObjectSpace entirely; the weak
|
|
6
|
+
# registry needs WeakMap, so the strong Hash (with its
|
|
7
|
+
# wholesale-clear valve) covers that platform.
|
|
8
|
+
WEAK_WRAPPERS = !defined?(ObjectSpace::WeakMap).nil?
|
|
5
9
|
attr_reader :config
|
|
6
10
|
|
|
7
11
|
def initialize(adapter = nil)
|
|
@@ -14,7 +18,12 @@ module Moxml
|
|
|
14
18
|
# the same wrapper instead of allocating a fresh one per
|
|
15
19
|
# access. Keyed by object identity; re-keyed by
|
|
16
20
|
# Node#refresh_native! when an adapter swaps a native.
|
|
17
|
-
|
|
21
|
+
# WeakMap where available: entries die with their native —
|
|
22
|
+
# parse-and-drop workloads release wrappers, natives, and (via
|
|
23
|
+
# the binding's finalizer) the C subtrees, instead of pinning
|
|
24
|
+
# up to the old 65,536 wholesale-clear valve which also
|
|
25
|
+
# destroyed identity for live wrappers when it fired.
|
|
26
|
+
@wrappers = WEAK_WRAPPERS ? ObjectSpace::WeakMap.new : {}.compare_by_identity
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
def wrapper_for(native)
|
|
@@ -22,18 +31,26 @@ module Moxml
|
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
def register_wrapper(native, wrapper)
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
34
|
+
# Adapter opt-in. Adapters whose natives are recreated per
|
|
35
|
+
# access (libxml mints fresh Ruby objects for the same C node)
|
|
36
|
+
# opt out so the map does not accumulate dead entries; the
|
|
37
|
+
# default is opt-in.
|
|
29
38
|
return if @config&.adapter&.wrappers_recyclable? == false
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
# The strong fallback (Opal) needs the wholesale-clear valve;
|
|
41
|
+
# the WeakMap registry is self-cleaning.
|
|
42
|
+
@wrappers.clear if @wrappers.is_a?(Hash) && @wrappers.size >= 65_536
|
|
32
43
|
@wrappers[native] = wrapper
|
|
33
44
|
end
|
|
34
45
|
|
|
46
|
+
# WeakMap has no delete — a nil value tombstones the entry (reads
|
|
47
|
+
# as a miss) and dies with the native like any other.
|
|
35
48
|
def unregister_wrapper(native)
|
|
36
|
-
|
|
49
|
+
if WEAK_WRAPPERS
|
|
50
|
+
@wrappers[native] = nil
|
|
51
|
+
else
|
|
52
|
+
@wrappers.delete(native)
|
|
53
|
+
end
|
|
37
54
|
end
|
|
38
55
|
|
|
39
56
|
def namespace_scope_generation
|
|
@@ -74,6 +91,29 @@ module Moxml
|
|
|
74
91
|
doc
|
|
75
92
|
end
|
|
76
93
|
|
|
94
|
+
# Tolerant HTML4/5 parsing into the standard DOM: implied end
|
|
95
|
+
# tags, void elements, case-insensitive lowercased names, the
|
|
96
|
+
# HTML named-entity table, synthesized html/head/body. Supported
|
|
97
|
+
# by adapters with an engine HTML mode (leptris >= 1.9.80,
|
|
98
|
+
# nokogiri); others raise Moxml::AdapterError.
|
|
99
|
+
def parse_html(html, options = {})
|
|
100
|
+
config.adapter.parse_html(html, options, self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Streaming incremental parse (leptris): yields each completed
|
|
104
|
+
# element while parsing — :top_level yields the root's children,
|
|
105
|
+
# :full_document every element in completion (post-order) order.
|
|
106
|
+
# Memory stays bounded by the largest subtree, not the document
|
|
107
|
+
# (iterparse_file streams the file C-side). Yielded elements are
|
|
108
|
+
# parentless and valid only inside the block.
|
|
109
|
+
def iterparse(xml, mode: :top_level, &block)
|
|
110
|
+
config.adapter.iterparse(xml, mode, self, &block)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def iterparse_file(path, mode: :top_level, &block)
|
|
114
|
+
config.adapter.iterparse_file(path, mode, self, &block)
|
|
115
|
+
end
|
|
116
|
+
|
|
77
117
|
# Parse then flatten in one call — see Moxml::Materializer
|
|
78
118
|
# (issue #132). Yields records; returns an Enumerator when no
|
|
79
119
|
# block is given.
|
|
@@ -128,6 +168,26 @@ module Moxml
|
|
|
128
168
|
Builder.new(self).build(&block)
|
|
129
169
|
end
|
|
130
170
|
|
|
171
|
+
# Frozen serialization defaults, memoized per context — Node#to_xml
|
|
172
|
+
# merged these from live config reads on every call (bulk element
|
|
173
|
+
# serialization pays a hash build + 4 chain derefs per element).
|
|
174
|
+
def default_serialize_options
|
|
175
|
+
@default_serialize_options ||= {
|
|
176
|
+
encoding: config.default_encoding,
|
|
177
|
+
indent: config.default_indent,
|
|
178
|
+
line_ending: config.default_line_ending,
|
|
179
|
+
expand_empty: true,
|
|
180
|
+
}.freeze
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The argless element.to_xml form resolves every option statically
|
|
184
|
+
# (no declaration for non-document nodes, context defaults for the
|
|
185
|
+
# rest) — this prebuilt frozen hash makes that path allocation-free.
|
|
186
|
+
def default_element_serialize_options
|
|
187
|
+
@default_element_serialize_options ||=
|
|
188
|
+
default_serialize_options.merge(no_declaration: true).freeze
|
|
189
|
+
end
|
|
190
|
+
|
|
131
191
|
private
|
|
132
192
|
|
|
133
193
|
def build_entity_registry
|
data/lib/moxml/document.rb
CHANGED
|
@@ -131,11 +131,11 @@ module Moxml
|
|
|
131
131
|
# Handle different result types:
|
|
132
132
|
# - Scalar values (from functions): return directly
|
|
133
133
|
# - NodeSet: already wrapped, return directly
|
|
134
|
-
# - Array: wrap in NodeSet
|
|
134
|
+
# - Array / LazyNodeSet: wrap in NodeSet
|
|
135
135
|
case result
|
|
136
136
|
when NodeSet, Float, String, TrueClass, FalseClass, NilClass
|
|
137
137
|
result
|
|
138
|
-
when Array
|
|
138
|
+
when Array, LazyNodeSet
|
|
139
139
|
NodeSet.new(result, context)
|
|
140
140
|
else
|
|
141
141
|
# For other types, try to wrap in NodeSet
|
data/lib/moxml/element.rb
CHANGED
|
@@ -46,7 +46,9 @@ module Moxml
|
|
|
46
46
|
# attributes only; prefixed names replace by namespace URI and
|
|
47
47
|
# require a declared prefix. Cache coherence is the resolver's.
|
|
48
48
|
def []=(name, value)
|
|
49
|
-
|
|
49
|
+
# Every assign path (native set or value=) ends in
|
|
50
|
+
# invalidate_attribute_cache!, which bumps the generation —
|
|
51
|
+
# a leading bump here was one per write.
|
|
50
52
|
Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
|
|
51
53
|
end
|
|
52
54
|
|
|
@@ -54,12 +56,25 @@ module Moxml
|
|
|
54
56
|
# bare names match only no-namespace attributes; prefixed names
|
|
55
57
|
# resolve through in-scope declarations to expanded names.
|
|
56
58
|
def [](name)
|
|
57
|
-
cache = attribute_read_cache
|
|
58
59
|
key = name.to_s
|
|
60
|
+
# Bare names on qualified-name engines: one adapter call with
|
|
61
|
+
# the adapter's own marker restoration (bare_attr_value). The
|
|
62
|
+
# resolver's expanded-name semantics only pay for prefixed
|
|
63
|
+
# names, where resolution is real work.
|
|
64
|
+
adapter = self.adapter
|
|
65
|
+
if !key.include?(":") && adapter.bare_get_qname_safe?
|
|
66
|
+
return adapter.bare_attr_value(@native, key)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
cache = attribute_read_cache
|
|
59
70
|
unless cache.key?(key)
|
|
60
|
-
cache[key] =
|
|
71
|
+
cache[key] = if key.include?(":")
|
|
72
|
+
Moxml::AttributeResolver.resolve_value(self, key)
|
|
73
|
+
else
|
|
74
|
+
Moxml::AttributeResolver.resolve(self, key)&.value
|
|
75
|
+
end
|
|
61
76
|
end
|
|
62
|
-
cache[key]
|
|
77
|
+
cache[key]
|
|
63
78
|
end
|
|
64
79
|
|
|
65
80
|
def attribute(name)
|
|
@@ -92,11 +107,41 @@ module Moxml
|
|
|
92
107
|
end
|
|
93
108
|
|
|
94
109
|
def remove_attribute(name)
|
|
95
|
-
|
|
110
|
+
# Both remove paths end in invalidate_attribute_cache!, which
|
|
111
|
+
# bumps the generation.
|
|
96
112
|
Moxml::AttributeResolver.remove(self, name)
|
|
97
113
|
self
|
|
98
114
|
end
|
|
99
115
|
|
|
116
|
+
# Append a raw XML fragment's top-level nodes as children.
|
|
117
|
+
#
|
|
118
|
+
# Bulk construction rides the engine's parser instead of
|
|
119
|
+
# per-node create/attach calls — measured 2.0x faster than
|
|
120
|
+
# per-node building through the same adapter, and faster than
|
|
121
|
+
# per-node building on Nokogiri (the parse is C; the per-node
|
|
122
|
+
# path pays an FFI crossing per node).
|
|
123
|
+
#
|
|
124
|
+
# The fragment must be namespace-self-contained (declarations
|
|
125
|
+
# inside it ride along with the moved subtrees; prefixes relying
|
|
126
|
+
# on THIS element's scope must be spelled out in the fragment)
|
|
127
|
+
# and well-formed as the content of a single wrapper element —
|
|
128
|
+
# declarations and doctypes raise ParseError.
|
|
129
|
+
#
|
|
130
|
+
# @return [Moxml::Element] self
|
|
131
|
+
def append_xml(fragment)
|
|
132
|
+
if context.config.adapter_name == :ox
|
|
133
|
+
raise Moxml::AdapterError.new(
|
|
134
|
+
"append_xml is not supported by the Ox adapter (its customized node wrappers do not survive cross-document attachment)",
|
|
135
|
+
adapter: "Ox", operation: "append_xml",
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
wrapper = context.parse("<m>#{fragment}</m>")
|
|
140
|
+
children = wrapper.root.children.to_a
|
|
141
|
+
children.each { |child| add_child(child) }
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
100
145
|
def add_namespace(prefix, uri)
|
|
101
146
|
adapter.create_namespace(@native, prefix, uri,
|
|
102
147
|
namespace_validation_mode: context.config.namespace_validation_mode)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Marker for an adapter's native node-set result that NodeSet can
|
|
5
|
+
# hold without materializing the natives. The leptris binding's
|
|
6
|
+
# NodeSet mints one Ruby wrapper per result node in #to_a — the
|
|
7
|
+
# whole 900-node result set is built even when the caller only
|
|
8
|
+
# asks .size or .first — so the adapter hands the native set
|
|
9
|
+
# through instead. Contract: responds to length, empty?, [],
|
|
10
|
+
# each, to_a (yielding natives).
|
|
11
|
+
module LazyNodeSet
|
|
12
|
+
def size
|
|
13
|
+
length
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -25,8 +25,13 @@ module Moxml
|
|
|
25
25
|
@monitor = Monitor.new
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Reads are lock-free: MRI Hash#[] is atomic under the GVL, and
|
|
29
|
+
# the only mid-write race (set assigns the inner hash before its
|
|
30
|
+
# key) resolves to a miss — the same answer as "not set yet",
|
|
31
|
+
# which every reader treats conservatively. This method sits on
|
|
32
|
+
# per-element hot paths (entity_bearing? on every serialize).
|
|
28
33
|
def get(native, key)
|
|
29
|
-
@
|
|
34
|
+
@data[native.object_id]&.[](key)
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def set(native, key, value)
|
|
@@ -39,7 +44,8 @@ module Moxml
|
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def key?(native, key)
|
|
42
|
-
|
|
47
|
+
h = @data[native.object_id]
|
|
48
|
+
!h.nil? && h.key?(key)
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
def delete(native, key)
|
data/lib/moxml/node.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Moxml
|
|
|
35
35
|
def clear_native_memo!
|
|
36
36
|
@name = nil
|
|
37
37
|
@attribute_cache = nil
|
|
38
|
+
@entity_bearing_gen = nil
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def document
|
|
@@ -64,9 +65,12 @@ module Moxml
|
|
|
64
65
|
def add_child(node)
|
|
65
66
|
node = prepare_node(node)
|
|
66
67
|
adapter.add_child(@native, node.native)
|
|
67
|
-
# Refresh native in case adapter changed identity (e.g., LibXML
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
# Refresh native in case adapter changed identity (e.g., LibXML
|
|
69
|
+
# doc.root=); stable-identity adapters skip the round trip.
|
|
70
|
+
unless adapter.native_identity_stable?
|
|
71
|
+
refreshed = adapter.actual_native(node.native, @native)
|
|
72
|
+
node.refresh_native!(refreshed) if refreshed && refreshed != node.native
|
|
73
|
+
end
|
|
70
74
|
node.parent_node = self
|
|
71
75
|
# The adopted subtree's in-scope namespaces changed
|
|
72
76
|
node.invalidate_namespace_cache!
|
|
@@ -113,23 +117,43 @@ module Moxml
|
|
|
113
117
|
# Determine if we should include XML declaration
|
|
114
118
|
# For Document nodes: check native then wrapper, unless explicitly overridden
|
|
115
119
|
# For other nodes: default to no declaration unless explicitly set
|
|
116
|
-
serialize_options =
|
|
117
|
-
|
|
120
|
+
serialize_options = if options.empty? && !is_a?(Document)
|
|
121
|
+
context.default_element_serialize_options
|
|
122
|
+
else
|
|
123
|
+
merged = context.default_serialize_options.merge(options)
|
|
124
|
+
merged[:no_declaration] = !should_include_declaration?(options)
|
|
125
|
+
merged
|
|
126
|
+
end
|
|
118
127
|
|
|
119
128
|
result = adapter.serialize(@native, serialize_options)
|
|
120
129
|
result = apply_line_ending(result, serialize_options[:line_ending])
|
|
121
130
|
|
|
122
131
|
# Restore entity markers to named entity references; skipped
|
|
123
132
|
# when the adapter knows the document carries no markers.
|
|
124
|
-
result = adapter.restore_entities(result) if
|
|
133
|
+
result = adapter.restore_entities(result) if entity_bearing?
|
|
125
134
|
result
|
|
126
135
|
end
|
|
127
136
|
|
|
137
|
+
# Memoized against the adapter's serialize generation — the
|
|
138
|
+
# entity-marker flag flips at parse and entity-reference mint,
|
|
139
|
+
# both adapter-level, and the generation bump is the invalidation
|
|
140
|
+
# signal. Adapters with static answers (base class) never bump.
|
|
141
|
+
def entity_bearing?
|
|
142
|
+
gen = adapter.serialize_generation
|
|
143
|
+
if @entity_bearing_gen == gen
|
|
144
|
+
@entity_bearing
|
|
145
|
+
else
|
|
146
|
+
@entity_bearing_gen = gen
|
|
147
|
+
@entity_bearing = adapter.entity_bearing?(@native)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
128
151
|
def xpath(expression, namespaces = {})
|
|
129
152
|
result = adapter.xpath(@native, expression, namespaces)
|
|
130
|
-
# Adapter contract: Array<native> | scalar.
|
|
131
|
-
# string-length(), booleans) pass through
|
|
132
|
-
|
|
153
|
+
# Adapter contract: Array<native> | LazyNodeSet | scalar.
|
|
154
|
+
# Scalars (count(), string-length(), booleans) pass through
|
|
155
|
+
# unwrapped; the set forms wrap lazily.
|
|
156
|
+
result.is_a?(Array) || result.is_a?(LazyNodeSet) ? NodeSet.new(result, context) : result
|
|
133
157
|
end
|
|
134
158
|
|
|
135
159
|
# Flattened post-order records for this subtree without
|
|
@@ -459,19 +483,6 @@ module Moxml
|
|
|
459
483
|
end
|
|
460
484
|
end
|
|
461
485
|
|
|
462
|
-
def default_options
|
|
463
|
-
{
|
|
464
|
-
encoding: context.config.default_encoding,
|
|
465
|
-
indent: context.config.default_indent,
|
|
466
|
-
line_ending: context.config.default_line_ending,
|
|
467
|
-
# The short format of empty tags in Oga and Nokogiri isn't configurable
|
|
468
|
-
# Oga: <empty /> (with a space)
|
|
469
|
-
# Nokogiri: <empty/> (without a space)
|
|
470
|
-
# The expanded format is enforced to avoid this conflict
|
|
471
|
-
expand_empty: true,
|
|
472
|
-
}
|
|
473
|
-
end
|
|
474
|
-
|
|
475
486
|
def should_include_declaration?(options)
|
|
476
487
|
return options[:declaration] if options.key?(:declaration)
|
|
477
488
|
return options.fetch(:declaration, false) unless is_a?(Document)
|