moxml 0.5.30 → 0.5.32
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 +68 -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/document_parts.rb +8 -0
- data/lib/moxml/adapter/leptris/markers.rb +4 -1
- data/lib/moxml/adapter/leptris/serialize.rb +71 -19
- 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 +247 -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
|
@@ -23,6 +23,58 @@ module Moxml
|
|
|
23
23
|
# the adapter no longer carries accommodation paths for them.
|
|
24
24
|
MINIMUM_BINDING_VERSION = "1.9.32"
|
|
25
25
|
|
|
26
|
+
# leptris_parse_html_string shipped in bindings 1.9.80
|
|
27
|
+
# (libleptris 1.9.75, engine #659) as Leptris::XML.parse_html.
|
|
28
|
+
HTML_PARSE_SUPPORTED =
|
|
29
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.80")
|
|
30
|
+
|
|
31
|
+
# Attribute-node xpath results carry proper wrappers since
|
|
32
|
+
# 1.9.105 (leptris-ruby#153: ResultAttr with name/value);
|
|
33
|
+
# before that the native gate routed them to the Ruby engine.
|
|
34
|
+
ATTR_RESULT_NATIVE =
|
|
35
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.105")
|
|
36
|
+
|
|
37
|
+
# Native C14N delegation probe: the engine's C14N must
|
|
38
|
+
# byte-match the Ruby reference (ported from canon) on a
|
|
39
|
+
# namespace-sorting + attributes + comments shape before
|
|
40
|
+
# Moxml::C14n hands the default path to it. The engine
|
|
41
|
+
# currently emits namespace declarations in document order
|
|
42
|
+
# instead of lexicographic (filed leptris/leptris#881); the
|
|
43
|
+
# probe auto-adopts the native path once a fixed build lands —
|
|
44
|
+
# no moxml release needed.
|
|
45
|
+
# Lazy, not load-time: the probe exercises the wrapper layer
|
|
46
|
+
# (Document/serialize/C14n), which is circular while THIS
|
|
47
|
+
# adapter file is still loading — the load-time form rescued
|
|
48
|
+
# to false on every build and masked a landed engine fix.
|
|
49
|
+
def self.native_c14n_byte_safe?
|
|
50
|
+
return @native_c14n_byte_safe unless @native_c14n_byte_safe.nil?
|
|
51
|
+
|
|
52
|
+
@native_c14n_byte_safe = begin
|
|
53
|
+
probe_xml = %(<?xml version="1.0"?><doc xmlns:p="urn:p" xmlns="urn:d" b="2" a="1"><e p:x="v" z="w">t & u</e><!-- c --></doc>)
|
|
54
|
+
native_doc = ::Leptris::XML::Document.parse(probe_xml)
|
|
55
|
+
native = native_doc.root.canonicalize(
|
|
56
|
+
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
57
|
+
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
58
|
+
)
|
|
59
|
+
wrapper = Moxml::Document.new(native_doc, Moxml::Context.new(:leptris))
|
|
60
|
+
reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
|
|
61
|
+
native == reference
|
|
62
|
+
rescue StandardError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Bumped whenever a document's :entity_markers flag is written
|
|
68
|
+
# (parse, parse_html, entity-reference mint) so wrapper-level
|
|
69
|
+
# entity_bearing? memos invalidate.
|
|
70
|
+
def self.serialize_generation
|
|
71
|
+
@serialize_generation ||= 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.bump_serialize_generation
|
|
75
|
+
@serialize_generation = serialize_generation + 1
|
|
76
|
+
end
|
|
77
|
+
|
|
26
78
|
# leptris-ruby#103: prefixed attribute tests inside predicates
|
|
27
79
|
# stopped resolving through the document's in-scope declarations
|
|
28
80
|
# on released 1.9.37–1.9.39; 1.9.40 (engine 1.9.14+) restored
|
|
@@ -108,6 +160,39 @@ module Moxml
|
|
|
108
160
|
doc.root = element
|
|
109
161
|
end
|
|
110
162
|
|
|
163
|
+
def bare_set_qname_safe?
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def bare_get_qname_safe?
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Expanded-name (URI + local) attribute VALUE lookup on the
|
|
172
|
+
# engine — the same match rule as the resolver (xmlns
|
|
173
|
+
# declarations invisible, no-namespace never matches a
|
|
174
|
+
# prefixed name), without materializing the attribute list.
|
|
175
|
+
def expanded_attr_value(element, uri, local)
|
|
176
|
+
element.attribute_ns(uri, local)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Fast bare-name read: the binding call plus marker
|
|
180
|
+
# restoration when the document carries them (entity-free
|
|
181
|
+
# documents — the common case — skip the scan; parentless
|
|
182
|
+
# iterparse elements are never bearing).
|
|
183
|
+
def bare_attr_value(element, name)
|
|
184
|
+
value = element[name.to_s]
|
|
185
|
+
if value.is_a?(String) && entity_bearing?(element)
|
|
186
|
+
restore_entities(value)
|
|
187
|
+
else
|
|
188
|
+
value
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def native_identity_stable?
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
111
196
|
def parse(xml, options = {}, _context = nil)
|
|
112
197
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
113
198
|
# The marker flag rides preprocess's own `&` scan — no
|
|
@@ -153,11 +238,59 @@ module Moxml
|
|
|
153
238
|
|
|
154
239
|
record_source_declaration(native_doc, processed)
|
|
155
240
|
attachments.set(native_doc, :entity_markers, entity_markers)
|
|
241
|
+
bump_serialize_generation
|
|
156
242
|
attachments.set(native_doc, :parse_errors, recover_errors) if recover_errors
|
|
157
243
|
|
|
158
244
|
doc
|
|
159
245
|
end
|
|
160
246
|
|
|
247
|
+
# Tolerant HTML4/5 parsing into the standard DOM (engine
|
|
248
|
+
# leptris/leptris#659): implied end tags, void elements,
|
|
249
|
+
# raw-text script/style, case-insensitive lowercased names,
|
|
250
|
+
# the HTML named-entity table, synthesized html/head/body.
|
|
251
|
+
# The engine decodes HTML entities directly into text — no
|
|
252
|
+
# marker pipeline, so the marker split scan is stood down.
|
|
253
|
+
# Incremental parse (libleptris v1.6/#586): yields completed
|
|
254
|
+
# elements as the parse runs; each prior subtree is released.
|
|
255
|
+
# Yielded elements are parentless (document nil) and valid
|
|
256
|
+
# only inside the block — the wrapper mirrors that lifetime.
|
|
257
|
+
def iterparse(xml, mode = :top_level, _context = nil, &block)
|
|
258
|
+
raise ArgumentError, "iterparse requires a block" unless block
|
|
259
|
+
|
|
260
|
+
ctx = _context || Context.new(:leptris)
|
|
261
|
+
::Leptris::XML::Iterparse.parse(xml, mode: mode) do |element|
|
|
262
|
+
yield(Node.wrap(element, ctx))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def iterparse_file(path, mode = :top_level, _context = nil, &block)
|
|
267
|
+
raise ArgumentError, "iterparse_file requires a block" unless block
|
|
268
|
+
|
|
269
|
+
ctx = _context || Context.new(:leptris)
|
|
270
|
+
::Leptris::XML::Iterparse.parse_file(path, mode: mode) do |element|
|
|
271
|
+
yield(Node.wrap(element, ctx))
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def parse_html(html, _options = {}, _context = nil)
|
|
276
|
+
unless HTML_PARSE_SUPPORTED
|
|
277
|
+
raise Moxml::AdapterError.new(
|
|
278
|
+
"HTML parsing requires leptris >= 1.9.80 (have #{::Leptris::VERSION})",
|
|
279
|
+
adapter: name, operation: "parse_html",
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
284
|
+
native_doc = begin
|
|
285
|
+
::Leptris::XML.parse_html(html_string)
|
|
286
|
+
rescue ::Leptris::XML::ParseError => e
|
|
287
|
+
raise Moxml::ParseError.new(e.message)
|
|
288
|
+
end
|
|
289
|
+
attachments.set(native_doc, :entity_markers, false)
|
|
290
|
+
bump_serialize_generation
|
|
291
|
+
Document.new(native_doc, _context || Context.new(:leptris))
|
|
292
|
+
end
|
|
293
|
+
|
|
161
294
|
# nil when no parse flag is requested — the binding treats a
|
|
162
295
|
# nil options hash as plain defaults (matching libxml2/
|
|
163
296
|
# Nokogiri semantics: blanks kept, no ATTLIST defaults).
|
|
@@ -393,7 +526,7 @@ module Moxml
|
|
|
393
526
|
when ::Leptris::XML::Element then :element
|
|
394
527
|
when ::Leptris::XML::CDATA then :cdata
|
|
395
528
|
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
|
|
396
|
-
when ::Leptris::XML::Attr then :attribute
|
|
529
|
+
when ::Leptris::XML::Attr, ::Leptris::XML::ResultAttr then :attribute
|
|
397
530
|
when ::Leptris::XML::Comment then :comment
|
|
398
531
|
when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
|
|
399
532
|
when ::Leptris::XML::Document then :document
|
|
@@ -455,7 +588,8 @@ module Moxml
|
|
|
455
588
|
# dominates cold children cost. Cross-document moves of
|
|
456
589
|
# marker-bearing text into an entity-free document degrade
|
|
457
590
|
# to literal text.
|
|
458
|
-
return natives if
|
|
591
|
+
return natives if node.document.nil? ||
|
|
592
|
+
attachments.get(node.document, :entity_markers) == false
|
|
459
593
|
|
|
460
594
|
split_entity_markers(natives, node)
|
|
461
595
|
end
|
|
@@ -561,6 +695,7 @@ module Moxml
|
|
|
561
695
|
marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
|
|
562
696
|
parent.add_child(marker)
|
|
563
697
|
attachments.set(parent.document, :entity_markers, true)
|
|
698
|
+
bump_serialize_generation
|
|
564
699
|
return child
|
|
565
700
|
end
|
|
566
701
|
child = parent.document.create_text_node(child) if child.is_a?(String)
|
|
@@ -772,8 +907,10 @@ module Moxml
|
|
|
772
907
|
end
|
|
773
908
|
case result
|
|
774
909
|
when ::Leptris::XML::NodeSet
|
|
775
|
-
|
|
776
|
-
|
|
910
|
+
# The binding's #to_a mints one Ruby wrapper per result
|
|
911
|
+
# node; hand the native set through so .size/.first stay
|
|
912
|
+
# native-side (LazyNodeSet holds it unmaterialized).
|
|
913
|
+
first_only ? result.first : result.extend(Moxml::LazyNodeSet)
|
|
777
914
|
else
|
|
778
915
|
result
|
|
779
916
|
end
|
|
@@ -785,6 +922,10 @@ module Moxml
|
|
|
785
922
|
end
|
|
786
923
|
|
|
787
924
|
def native_context_node?(node)
|
|
925
|
+
# Parentless elements (iterparse yields) have no document
|
|
926
|
+
# handle for the compiled eval — the Ruby engine owns them.
|
|
927
|
+
return false if node.is_a?(::Leptris::XML::Element) && node.document.nil?
|
|
928
|
+
|
|
788
929
|
node.is_a?(::Leptris::XML::Document) ||
|
|
789
930
|
node.is_a?(::Leptris::XML::Element)
|
|
790
931
|
end
|
|
@@ -815,6 +956,12 @@ module Moxml
|
|
|
815
956
|
next false if uses_xmlns_prefix?(ast)
|
|
816
957
|
next false if !PREFIXED_ATTR_PREDICATES_NATIVE && prefixed_attribute_test?(ast)
|
|
817
958
|
|
|
959
|
+
# Attribute-node results are native since 1.9.105
|
|
960
|
+
# (leptris-ruby#153: ResultAttr wrappers with name/value);
|
|
961
|
+
# older bindings returned generic Node wrappers whose
|
|
962
|
+
# #name raised, so the Ruby engine owned them.
|
|
963
|
+
next true if selects_attribute_results?(ast) && ATTR_RESULT_NATIVE
|
|
964
|
+
|
|
818
965
|
!selects_attribute_results?(ast)
|
|
819
966
|
end
|
|
820
967
|
rescue XPath::SyntaxError
|
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)
|