moxml 0.5.35 → 0.5.41
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/performance.adoc +61 -3
- data/lib/moxml/adapter/base.rb +22 -1
- data/lib/moxml/adapter/leptris/document_parts.rb +18 -1
- data/lib/moxml/adapter/leptris/markers.rb +10 -1
- data/lib/moxml/adapter/leptris/materialize.rb +20 -10
- data/lib/moxml/adapter/leptris/serialize.rb +10 -1
- data/lib/moxml/adapter/leptris.rb +338 -6
- data/lib/moxml/adapter/libxml.rb +1 -1
- data/lib/moxml/adapter/nokogiri.rb +1 -1
- data/lib/moxml/adapter/oga.rb +1 -1
- data/lib/moxml/adapter/ox.rb +1 -1
- data/lib/moxml/adapter/rexml.rb +1 -1
- data/lib/moxml/c14n.rb +1 -1
- data/lib/moxml/element.rb +4 -1
- data/lib/moxml/node.rb +32 -4
- data/lib/moxml/version.rb +1 -1
- data/spec/integration/contract_parity_spec.rb +10 -0
- data/spec/integration/shared_examples/node_wrappers/node_set_behavior.rb +1 -1
- data/spec/moxml/adapter/base_spec.rb +23 -0
- data/spec/moxml/adapter/leptris_spec.rb +22 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +4 -6
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d96e7e77957533fdb65727e5d244d038a6947b30a46563463b995114e4594faf
|
|
4
|
+
data.tar.gz: 11940d59f145433f37e0a5ea286055cf6d30167614291d007ff2d41dc68606a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25d5f4a19555bd8802f978a3586d2d4c7fc9770d918fbfee1777203d548c6ed8d0d67fa42fd4dcddc047675f2034482f1323542379fe61193358209a4f8670d0
|
|
7
|
+
data.tar.gz: be203488271f744cb405f70a48af237408870c433a7b5922c8cafb1f303b2a4a451d0b0f6c3c80ce249a9fe75ec98904c330ff3a0cda212587fb231aac255f53
|
|
@@ -232,10 +232,20 @@ generation; `xmlns` writes keep the global bump.
|
|
|
232
232
|
| Attribute write (`[]=`) | 822ns | 758ns (-8%) |
|
|
233
233
|
| Cold walk (parse + wrapper mint, 752 nodes) | 811µs | 699µs (-14%) |
|
|
234
234
|
| Mixed build+read (nokogiri) | 1355µs / 5267 allocs | 1283µs / 5267 allocs |
|
|
235
|
-
| `Element#text` read | 422ns |
|
|
235
|
+
| `Element#text` read | 422ns | 249ns |
|
|
236
236
|
| `create_text` allocations | 6/node | 5/node |
|
|
237
|
-
| Bare attribute read `[]` | 714ns |
|
|
238
|
-
| Consumer pipeline (parse + typed walk) | 6112µs (0.50x) |
|
|
237
|
+
| Bare attribute read `[]` | 714ns | 385ns |
|
|
238
|
+
| Consumer pipeline (parse + typed walk) | 6112µs (0.50x) | 3536µs (1.07x) |
|
|
239
|
+
|
|
240
|
+
The 1.9.163.2 round added: UTF-8 native strings (the per-read
|
|
241
|
+
dup+force_encoding retag dropped — 3000 allocations per pipeline
|
|
242
|
+
run), the serialize generation as an attr_reader (the entity guard
|
|
243
|
+
consults it on every read), and a per-wrapper capability memo for
|
|
244
|
+
the bare-qname predicate. Reads sit at ~0.5x of raw Nokogiri with
|
|
245
|
+
the binding floor (`NativeNode#[]` 110ns, `#content` 85ns)
|
|
246
|
+
accounting for the budget's remainder — the 2x-per-operation ask
|
|
247
|
+
for reads, writes, and create+append is filed with the floor table
|
|
248
|
+
as leptris-ruby#204.
|
|
239
249
|
|
|
240
250
|
The pipeline jump has two causes: leptris 1.9.157-1.9.162 (inline
|
|
241
251
|
attribute values, TLS hoist, inline edge encoding, O(this-document)
|
|
@@ -253,6 +263,54 @@ read cache materializes lazily). The remaining cold-walk gap is
|
|
|
253
263
|
binding-side (its per-node Ruby wrapper construction) — tracked
|
|
254
264
|
upstream as the TypedData work.
|
|
255
265
|
|
|
266
|
+
=== Native read layer (leptris >= 1.9.162.6)
|
|
267
|
+
|
|
268
|
+
The leptris adapter adopts the binding's opt-in native read layer
|
|
269
|
+
(leptris-ruby#185): TypedData node wrappers with C-bound hot reads
|
|
270
|
+
and bulk children construction — one wrapper-cache round-trip for a
|
|
271
|
+
whole child list, zero per-node FFI. Element natives minted from
|
|
272
|
+
`#root` and `children` are native-layer nodes; the C tree is shared
|
|
273
|
+
with the binding, so writes made through the bridged binding nodes
|
|
274
|
+
(a `to_binding` wrap over the same pointer) are visible to native
|
|
275
|
+
reads. Engines without the layer, or ruby-platform installs without
|
|
276
|
+
the compiled bundle, stay on the binding path unchanged.
|
|
277
|
+
|
|
278
|
+
| path | binding path | native layer |
|
|
279
|
+
| Consumer pipeline (parse + typed walk) | 6112µs (0.50x nokogiri) | **3380µs (1.00-1.04x)** |
|
|
280
|
+
| Pipeline allocations | 13564 | **9813 (0.69x nokogiri)** |
|
|
281
|
+
| Cold walk (parse + mint) | 738µs | **509µs** |
|
|
282
|
+
| Raw walk (binding vs native, upstream's fixture) | 64.3µs/doc | 22.8µs/doc |
|
|
283
|
+
|
|
284
|
+
Entity-marker documents fall back to the binding children path (the
|
|
285
|
+
marker split materializes TextSegments); the wrapper's
|
|
286
|
+
`entity_bearing?` memo decides, so the check costs no per-call
|
|
287
|
+
climb. `Node#==` and the adapter protocol's `same_node?` compare
|
|
288
|
+
C-node addresses — two wrapper classes over one node compare equal.
|
|
289
|
+
|
|
290
|
+
The 1.9.162.6–163.1 bindings truncate bulk children at 512
|
|
291
|
+
(leptris-ruby#202): moxml fetches with its own count-then-copy
|
|
292
|
+
scratch there. 1.9.163.2 moved the bulk into C and fixed the
|
|
293
|
+
truncation — newer bindings use their C path (version-memoized,
|
|
294
|
+
cheaper than a per-call count+copy).
|
|
295
|
+
|
|
296
|
+
The native *builder* factories (native_create_element/text) are
|
|
297
|
+
deliberately not adopted: native mutations do not advance the
|
|
298
|
+
binding's document version, so the binding's memoized reads over
|
|
299
|
+
the same tree go stale, and attaching the created natives pays a
|
|
300
|
+
per-node `to_binding` bridge that costs more than the factory
|
|
301
|
+
saves (create+attach measured 5816 -> 7697ns vs nokogiri 1750).
|
|
302
|
+
|
|
303
|
+
Serialize (document, 30KB): passing `encoding: "UTF-8"` when it
|
|
304
|
+
equals the document's own ran a conversion pass for byte-identical
|
|
305
|
+
output (~21µs, skipped on 1.9.163.2+); the legacy
|
|
306
|
+
unescaped-ampersand probe (Linux 1.9.50 platform gem) stood down
|
|
307
|
+
below 1.9.60. 175 -> 149µs (~1.6x raw nokogiri). The dominant
|
|
308
|
+
remaining overhead is CRuby itself: a two-byte
|
|
309
|
+
`String#include?("/>")` costs ~34µs on 31KB — no memchr fast path
|
|
310
|
+
exists for multi-byte needles, and `match?` is slower still. A C
|
|
311
|
+
containment probe or a document-face `expand_empty` option would
|
|
312
|
+
close it (leptris-ruby#204, ask 4).
|
|
313
|
+
|
|
256
314
|
=== Prefer bulk paths for per-node conversion
|
|
257
315
|
|
|
258
316
|
Per-node Ruby iteration pays the wrapper tax per element. When the
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -116,6 +116,27 @@ module Moxml
|
|
|
116
116
|
)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Whether this adapter's #children accepts the
|
|
120
|
+
# entity_bearing: keyword (the built-ins do; downstream
|
|
121
|
+
# overrides may keep the pre-0.5.36 one-argument signature —
|
|
122
|
+
# issue #218). Reflected once per adapter class.
|
|
123
|
+
def children_accepts_entity_flag?
|
|
124
|
+
return @children_accepts_entity_flag unless @children_accepts_entity_flag.nil?
|
|
125
|
+
|
|
126
|
+
kinds = %i[key keyrest keyreq]
|
|
127
|
+
@children_accepts_entity_flag =
|
|
128
|
+
method(:children).parameters.any? do |kind, _name|
|
|
129
|
+
kinds.include?(kind)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Protocol-level native equality; engines with more than one
|
|
134
|
+
# wrapper class over one C node override (leptris native
|
|
135
|
+
# read layer). Shared adapter examples compare through this.
|
|
136
|
+
def same_node?(one, other)
|
|
137
|
+
one == other
|
|
138
|
+
end
|
|
139
|
+
|
|
119
140
|
# Backends without a native subtree digest answer nil —
|
|
120
141
|
# the wrapper contract Node#digest gates on (issue #173).
|
|
121
142
|
def digest(*)
|
|
@@ -272,7 +293,7 @@ namespace_validation_mode: :strict)
|
|
|
272
293
|
# Marker-tracking adapters override this so the post-serialize
|
|
273
294
|
# restore can skip its full-output scans on marker-free
|
|
274
295
|
# documents; the default stays conservative.
|
|
275
|
-
def entity_bearing?(_native)
|
|
296
|
+
def entity_bearing?(_native, _doc = nil)
|
|
276
297
|
true
|
|
277
298
|
end
|
|
278
299
|
|
|
@@ -38,6 +38,15 @@ module Moxml
|
|
|
38
38
|
|
|
39
39
|
texts = attachments.get(doc, :document_text)
|
|
40
40
|
children.concat(texts) if texts
|
|
41
|
+
|
|
42
|
+
# Canonicalize through the doc's address-keyed native
|
|
43
|
+
# cache: #root mints a NativeNode for the document
|
|
44
|
+
# element, and an uncanonicalized list would hand the
|
|
45
|
+
# binding twin instead — two wrappers over one node, and
|
|
46
|
+
# equal?-based exclusion (canon's document-element skip)
|
|
47
|
+
# silently breaks (issue #219). Mint-on-miss converges
|
|
48
|
+
# both accessors on the same native object.
|
|
49
|
+
children.map! { |child| canonical_native(doc, child) } if NATIVE_READ_LAYER
|
|
41
50
|
children
|
|
42
51
|
end
|
|
43
52
|
|
|
@@ -164,10 +173,18 @@ module Moxml
|
|
|
164
173
|
return nil
|
|
165
174
|
end
|
|
166
175
|
|
|
176
|
+
# Passing encoding when it equals the document's own is a
|
|
177
|
+
# no-op conversion the serializer still pays (~20% of a
|
|
178
|
+
# 31KB document serialize); nil skips it. Byte-equality of
|
|
179
|
+
# both forms verified on 1.9.163.2 — gate to those builds.
|
|
180
|
+
encoding = options[:encoding]
|
|
181
|
+
if NATIVE_STRINGS_UTF8 && encoding.to_s.casecmp?("UTF-8")
|
|
182
|
+
encoding = nil
|
|
183
|
+
end
|
|
167
184
|
kwargs = {
|
|
168
185
|
indent: options.fetch(:indent, 0),
|
|
169
186
|
no_decl: !include_decl,
|
|
170
|
-
encoding:
|
|
187
|
+
encoding: encoding,
|
|
171
188
|
}
|
|
172
189
|
if INDENT_UNIT_SUPPORTED && options[:indent_text].is_a?(String)
|
|
173
190
|
kwargs[:indent_text] = options[:indent_text]
|
|
@@ -8,7 +8,16 @@ module Moxml
|
|
|
8
8
|
# the ER builder path flips it, and the split/restore scans
|
|
9
9
|
# consult it. Customized natives only exist as split products
|
|
10
10
|
# of marker-bearing text, so they always report true.
|
|
11
|
-
|
|
11
|
+
# doc is the binding document when the wrapper could resolve
|
|
12
|
+
# it through its parent chain; the C climb is the fallback.
|
|
13
|
+
def entity_bearing?(native, doc = nil)
|
|
14
|
+
if NATIVE_READ_LAYER && native.is_a?(::Leptris::XML::NativeNode)
|
|
15
|
+
doc ||= doc_for(native)
|
|
16
|
+
return false if doc.nil?
|
|
17
|
+
|
|
18
|
+
return attachments.get(doc, :entity_markers) != false
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
case native
|
|
13
22
|
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
14
23
|
CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
|
|
@@ -39,16 +39,26 @@ module Moxml
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def materialize_fields(native, buffers, &block)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
if NATIVE_READ_LAYER && native.is_a?(::Leptris::XML::NativeNode)
|
|
43
|
+
# The native read layer mints NativeNodes from #root and
|
|
44
|
+
# #children; they expose address, not document/c_ptr
|
|
45
|
+
# (issue #213 — the wrapper-level materialize path).
|
|
46
|
+
doc = doc_for(native)
|
|
47
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
48
|
+
|
|
49
|
+
root_ptr = ::FFI::Pointer.new(native.address)
|
|
50
|
+
else
|
|
51
|
+
doc = native.is_a?(::Leptris::XML::Document) ? native : native.document
|
|
52
|
+
# Marker-bearing text needs the split pipeline (children-level
|
|
53
|
+
# ER expansion); the bulk path has no marker handling.
|
|
54
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
55
|
+
|
|
56
|
+
root_ptr = if native.is_a?(::Leptris::XML::Document)
|
|
57
|
+
doc.root&.c_ptr
|
|
58
|
+
else
|
|
59
|
+
native.c_ptr
|
|
60
|
+
end
|
|
61
|
+
end
|
|
52
62
|
return nil unless root_ptr
|
|
53
63
|
|
|
54
64
|
walk_fields(root_ptr, 0, buffers,
|
|
@@ -30,6 +30,7 @@ module Moxml
|
|
|
30
30
|
EMPTY_ELEMENT_RE = %r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
|
|
31
31
|
|
|
32
32
|
def serialize(node, options = {})
|
|
33
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
33
34
|
# Entity restoration belongs to the wrapper layer
|
|
34
35
|
# (Node#to_xml runs adapter.restore_entities for every
|
|
35
36
|
# adapter); doing it here scanned the output a second time.
|
|
@@ -134,6 +135,13 @@ module Moxml
|
|
|
134
135
|
# platform gem) emit text-content ampersands unescaped; the
|
|
135
136
|
# detection below is a no-op on correct builds.
|
|
136
137
|
RAW_AMP_RE = /&(?!#{Entity::NAME_PATTERN};|#\d+;|#x[0-9A-Fa-f]+;)/
|
|
138
|
+
# The unescaped-ampersand emission was observed on the Linux
|
|
139
|
+
# 1.9.50 platform gem; correct builds make the probe a
|
|
140
|
+
# no-op that still pays its scan (~2µs per document
|
|
141
|
+
# serialize). Stand it down on modern engines — the
|
|
142
|
+
# consistency round-trips fail loudly on any regression.
|
|
143
|
+
RAW_AMP_GUARD_ACTIVE =
|
|
144
|
+
Gem::Version.new(::Leptris::VERSION) < Gem::Version.new("1.9.60")
|
|
137
145
|
|
|
138
146
|
# A raw "<" in text position — the engine intermittently
|
|
139
147
|
# lost the escape when parsing under allocation pressure
|
|
@@ -158,7 +166,8 @@ module Moxml
|
|
|
158
166
|
# Corruption guards: the 1-char ampersand probe is ~1µs
|
|
159
167
|
# (memchr-class); the raw-< scan runs only on builds that
|
|
160
168
|
# still carry the parse race (leptris-ruby#131).
|
|
161
|
-
needs_amp =
|
|
169
|
+
needs_amp = RAW_AMP_GUARD_ACTIVE &&
|
|
170
|
+
xml.include?("&") && xml.match?(RAW_AMP_RE)
|
|
162
171
|
needs_lt = RAW_LT_GUARD_ACTIVE && xml.match?(RAW_LT_TRIGGER_RE)
|
|
163
172
|
return xml unless needs_apos || needs_expand || needs_amp || needs_lt
|
|
164
173
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
3
5
|
return if RUBY_ENGINE == "opal"
|
|
4
6
|
|
|
5
7
|
require "stringio"
|
|
@@ -40,6 +42,106 @@ module Moxml
|
|
|
40
42
|
DIGEST_SUPPORTED =
|
|
41
43
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
|
|
42
44
|
|
|
45
|
+
# Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
|
|
46
|
+
# TypedData node wrappers with C-bound hot reads and bulk
|
|
47
|
+
# children construction. Minted from #root downward; the C
|
|
48
|
+
# tree is shared with the binding, so writes made through the
|
|
49
|
+
# bridged binding nodes are visible to native reads. Installs
|
|
50
|
+
# without the compiled bundle (ruby-platform gem) simply stay
|
|
51
|
+
# on the binding path.
|
|
52
|
+
begin
|
|
53
|
+
require "leptris/xml/native_layer"
|
|
54
|
+
rescue LoadError, StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
NATIVE_READ_LAYER =
|
|
58
|
+
defined?(::Leptris::XML::NativeNode) &&
|
|
59
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
|
|
60
|
+
|
|
61
|
+
# 1.9.163.2's native layer returns UTF-8, unfrozen strings
|
|
62
|
+
# (1.9.162.x answered ASCII-8BIT — the reads retagged with a
|
|
63
|
+
# dup+force_encoding per call).
|
|
64
|
+
NATIVE_STRINGS_UTF8 =
|
|
65
|
+
defined?(::Leptris::XML::NativeNode) &&
|
|
66
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
|
|
67
|
+
|
|
68
|
+
# 1.9.163.2 moved the native bulk children into C
|
|
69
|
+
# (Native.bulk_children) and fixed the 512 truncation
|
|
70
|
+
# (leptris-ruby#202). The 162.6-163.1 window carries the
|
|
71
|
+
# binding-side truncation, so moxml fetches with its own
|
|
72
|
+
# count-then-copy scratch there; newer bindings use their
|
|
73
|
+
# C bulk path (version-memoized, cheaper than a per-call
|
|
74
|
+
# count+copy).
|
|
75
|
+
NATIVE_BULK_FIXED =
|
|
76
|
+
defined?(::Leptris::XML::NativeNode) &&
|
|
77
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
|
|
78
|
+
|
|
79
|
+
# Defined unconditionally: the body self-guards, and call
|
|
80
|
+
# sites must never depend on the layer having loaded
|
|
81
|
+
# (issue #217 — binding-only installs crashed on the
|
|
82
|
+
# unguarded bridges).
|
|
83
|
+
class << self
|
|
84
|
+
# Binding node for any native: identity for binding nodes
|
|
85
|
+
# (and on installs without the native layer — the constant
|
|
86
|
+
# check short-circuits), a Node.wrap over the shared C
|
|
87
|
+
# pointer for NativeNodes.
|
|
88
|
+
def to_binding(node)
|
|
89
|
+
return node unless NATIVE_READ_LAYER &&
|
|
90
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
91
|
+
|
|
92
|
+
doc = doc_for(node)
|
|
93
|
+
ptr = ::FFI::Pointer.new(node.address)
|
|
94
|
+
return ::Leptris::XML::Node.wrap(ptr, doc) if doc
|
|
95
|
+
|
|
96
|
+
::Leptris::XML::Node.wrap(ptr, nil)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if NATIVE_READ_LAYER
|
|
101
|
+
# root NativeNode -> binding document (recorded at #root
|
|
102
|
+
# mint; the native layer exposes no document accessor).
|
|
103
|
+
@native_doc_roots = ObjectSpace::WeakMap.new
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
def record_native_doc(root_native, doc)
|
|
107
|
+
@native_doc_roots[root_native] = doc
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Protocol-level node equality: one engine can hand out
|
|
111
|
+
# two wrapper classes over the same C node (native layer +
|
|
112
|
+
# binding), so raw == is not identity across the seam.
|
|
113
|
+
def same_node?(one, other)
|
|
114
|
+
return true if one.equal?(other)
|
|
115
|
+
|
|
116
|
+
if NATIVE_READ_LAYER
|
|
117
|
+
native_one = one.is_a?(::Leptris::XML::NativeNode)
|
|
118
|
+
native_other = other.is_a?(::Leptris::XML::NativeNode)
|
|
119
|
+
if native_one || native_other
|
|
120
|
+
address_one = native_one ? one.address : one.c_ptr.address
|
|
121
|
+
address_other = native_other ? other.address : other.c_ptr.address
|
|
122
|
+
return address_one == address_other
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
one == other
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Binding document for a NativeNode subtree: climb parents
|
|
130
|
+
# to the root (C-bound reads) and look the doc up in the
|
|
131
|
+
# root registry. Detached subtrees answer nil.
|
|
132
|
+
def doc_for(node)
|
|
133
|
+
current = node
|
|
134
|
+
current = current.parent while current&.parent
|
|
135
|
+
@native_doc_roots[current]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Binding node for any native: identity for binding nodes, a
|
|
141
|
+
# Node.wrap over the shared C pointer for NativeNodes (used by
|
|
142
|
+
# the write/serialize/namespace paths the native layer does
|
|
143
|
+
# not carry).
|
|
144
|
+
|
|
43
145
|
# Native C14N delegation probe: the engine's C14N must
|
|
44
146
|
# byte-match the Ruby reference (ported from canon) on a
|
|
45
147
|
# namespace-sorting + attributes + comments shape before
|
|
@@ -73,12 +175,15 @@ module Moxml
|
|
|
73
175
|
# Bumped whenever a document's :entity_markers flag is written
|
|
74
176
|
# (parse, parse_html, entity-reference mint) so wrapper-level
|
|
75
177
|
# entity_bearing? memos invalidate.
|
|
76
|
-
|
|
77
|
-
|
|
178
|
+
# attr_reader beats the ||= memo on every guarded read (the
|
|
179
|
+
# entity guard consults this per bare read / text read).
|
|
180
|
+
class << self
|
|
181
|
+
attr_reader :serialize_generation
|
|
78
182
|
end
|
|
183
|
+
@serialize_generation = 0
|
|
79
184
|
|
|
80
185
|
def self.bump_serialize_generation
|
|
81
|
-
@serialize_generation
|
|
186
|
+
@serialize_generation += 1
|
|
82
187
|
end
|
|
83
188
|
|
|
84
189
|
# leptris-ruby#103: prefixed attribute tests inside predicates
|
|
@@ -191,7 +296,14 @@ module Moxml
|
|
|
191
296
|
# document-plus-attachment probe here cost more than the
|
|
192
297
|
# read itself).
|
|
193
298
|
def bare_attr_value(element, name)
|
|
194
|
-
element[name.to_s]
|
|
299
|
+
value = element[name.to_s]
|
|
300
|
+
if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
|
|
301
|
+
element.is_a?(::Leptris::XML::NativeNode) &&
|
|
302
|
+
value.is_a?(String)
|
|
303
|
+
return value.dup.force_encoding(Encoding::UTF_8)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
value
|
|
195
307
|
end
|
|
196
308
|
|
|
197
309
|
def native_identity_stable?
|
|
@@ -365,6 +477,13 @@ module Moxml
|
|
|
365
477
|
::Leptris::XML::Document.create
|
|
366
478
|
end
|
|
367
479
|
|
|
480
|
+
# The native builder factories (native_create_element/text)
|
|
481
|
+
# are deliberately NOT adopted: native mutations do not
|
|
482
|
+
# advance the binding's document version, so the binding's
|
|
483
|
+
# memoized reads over the same tree go stale; attaching the
|
|
484
|
+
# created natives also pays a per-node to_binding bridge
|
|
485
|
+
# that costs more than the factory saves (create+attach
|
|
486
|
+
# measured 5816 -> 7697ns vs nokogiri 1750).
|
|
368
487
|
def create_native_element(name, owner_doc = nil)
|
|
369
488
|
(owner_doc || create_document).create_element(name.to_s)
|
|
370
489
|
end
|
|
@@ -428,10 +547,12 @@ module Moxml
|
|
|
428
547
|
end
|
|
429
548
|
|
|
430
549
|
def create_native_namespace(element, prefix, uri)
|
|
550
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
431
551
|
element.add_namespace_definition(prefix, uri)
|
|
432
552
|
end
|
|
433
553
|
|
|
434
554
|
def set_namespace(node, namespace)
|
|
555
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
435
556
|
return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
|
|
436
557
|
|
|
437
558
|
element = node
|
|
@@ -498,6 +619,8 @@ module Moxml
|
|
|
498
619
|
end
|
|
499
620
|
|
|
500
621
|
def namespace(node)
|
|
622
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
623
|
+
|
|
501
624
|
# The binding resolves Attr#namespace to the declaration but
|
|
502
625
|
# without the prefix; when the qualified name carries one,
|
|
503
626
|
# prefer the in-scope declaration that binds it so wrappers
|
|
@@ -549,6 +672,16 @@ module Moxml
|
|
|
549
672
|
end
|
|
550
673
|
|
|
551
674
|
def node_type(node)
|
|
675
|
+
if NATIVE_READ_LAYER &&
|
|
676
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
677
|
+
type = node.node_type
|
|
678
|
+
# The native layer spells PIs :pi; the wrapper contract
|
|
679
|
+
# (node_type_map) says :processing_instruction.
|
|
680
|
+
return :processing_instruction if type == :pi
|
|
681
|
+
|
|
682
|
+
return type
|
|
683
|
+
end
|
|
684
|
+
|
|
552
685
|
# Frequency-ordered: elements and text dominate every real
|
|
553
686
|
# document, and Node.wrap dispatches here once per cold
|
|
554
687
|
# wrap. CDATA must precede Text (CDATA < Text in the
|
|
@@ -574,6 +707,9 @@ module Moxml
|
|
|
574
707
|
# lightweight Attr triples answer nil.
|
|
575
708
|
def digest(node, drop_ws_text: false)
|
|
576
709
|
return nil unless DIGEST_SUPPORTED
|
|
710
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
711
|
+
return to_binding(node).digest(drop_ws: drop_ws_text)
|
|
712
|
+
end
|
|
577
713
|
return nil unless node.is_a?(::Leptris::XML::Node) &&
|
|
578
714
|
!node.is_a?(::Leptris::XML::ResultAttr)
|
|
579
715
|
|
|
@@ -584,10 +720,20 @@ module Moxml
|
|
|
584
720
|
return node.root_name if node.is_a?(::Leptris::XML::DocType)
|
|
585
721
|
return node.target if node.is_a?(CustomizedLeptris::DocumentPI)
|
|
586
722
|
|
|
723
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
724
|
+
# PIs expose no name through the native layer.
|
|
725
|
+
return to_binding(node).target.to_s if node.node_type == :pi
|
|
726
|
+
|
|
727
|
+
return node.name if NATIVE_STRINGS_UTF8
|
|
728
|
+
|
|
729
|
+
node.name.dup.force_encoding(Encoding::UTF_8)
|
|
730
|
+
end
|
|
731
|
+
|
|
587
732
|
node.name.to_s.dup.force_encoding("UTF-8")
|
|
588
733
|
end
|
|
589
734
|
|
|
590
735
|
def set_node_name(node, name)
|
|
736
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
591
737
|
case node
|
|
592
738
|
when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
|
|
593
739
|
else node.name = name
|
|
@@ -595,6 +741,7 @@ module Moxml
|
|
|
595
741
|
end
|
|
596
742
|
|
|
597
743
|
def duplicate_node(node)
|
|
744
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
598
745
|
case node
|
|
599
746
|
when CustomizedLeptris::Declaration
|
|
600
747
|
CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
|
|
@@ -609,12 +756,42 @@ module Moxml
|
|
|
609
756
|
end
|
|
610
757
|
end
|
|
611
758
|
|
|
612
|
-
def children(node)
|
|
759
|
+
def children(node, entity_bearing: false)
|
|
760
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
761
|
+
# Bulk C children through moxml's own scratch (see
|
|
762
|
+
# NATIVE_READ_LAYER note above). Entity-marker documents
|
|
763
|
+
# bridge each child for the marker split — the
|
|
764
|
+
# TextSegment reconstruction reads binding text nodes;
|
|
765
|
+
# the wrapper's memo supplies the flag, so deriving it
|
|
766
|
+
# here costs no C parent climb per call.
|
|
767
|
+
natives = if NATIVE_BULK_FIXED
|
|
768
|
+
node.children.to_a
|
|
769
|
+
else
|
|
770
|
+
bulk_native_children(node)
|
|
771
|
+
end
|
|
772
|
+
return natives unless entity_bearing
|
|
773
|
+
|
|
774
|
+
# Entity-marker documents: bridge for the marker split
|
|
775
|
+
# (TextSegment reconstruction reads binding text nodes).
|
|
776
|
+
doc = doc_for(node)
|
|
777
|
+
bound_children = natives.map { |child| to_binding(child) }
|
|
778
|
+
if doc && attachments.get(doc, :entity_markers) == false
|
|
779
|
+
return bound_children
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
return split_entity_markers(bound_children, to_binding(node))
|
|
783
|
+
end
|
|
613
784
|
# Frequency-ordered: elements dominate every walk and paid
|
|
614
785
|
# ten failed compares to reach the else arm.
|
|
615
786
|
case node
|
|
616
787
|
when ::Leptris::XML::Element
|
|
617
788
|
natives = node.children.to_a
|
|
789
|
+
if NATIVE_READ_LAYER && natives.size == 512
|
|
790
|
+
# Binding scratch truncation (leptris-ruby#202): a
|
|
791
|
+
# full batch is ambiguous — refetch through moxml's
|
|
792
|
+
# own buffers to be sure.
|
|
793
|
+
natives = bulk_binding_children(node)
|
|
794
|
+
end
|
|
618
795
|
# Parse records whether the preprocessed source held any
|
|
619
796
|
# entity markers, and the ER builder path flips the flag
|
|
620
797
|
# when it mints one. A false flag lets traversal skip the
|
|
@@ -643,6 +820,16 @@ module Moxml
|
|
|
643
820
|
end
|
|
644
821
|
|
|
645
822
|
def parent(node)
|
|
823
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
824
|
+
parent = node.parent
|
|
825
|
+
return parent if parent
|
|
826
|
+
|
|
827
|
+
doc = doc_for(node)
|
|
828
|
+
return doc if doc&.root && doc.root.c_ptr.address == node.address
|
|
829
|
+
|
|
830
|
+
return nil
|
|
831
|
+
end
|
|
832
|
+
|
|
646
833
|
# Frequency-ordered: elements dominate navigation and paid
|
|
647
834
|
# three failed compares to reach the else arm.
|
|
648
835
|
case node
|
|
@@ -657,6 +844,99 @@ module Moxml
|
|
|
657
844
|
end
|
|
658
845
|
end
|
|
659
846
|
|
|
847
|
+
# Fallback for natives without a registered document: walk
|
|
848
|
+
# first_child/next_sibling at the FFI level and wrap binding
|
|
849
|
+
# nodes over the raw pointers.
|
|
850
|
+
def sibling_walk_children(node)
|
|
851
|
+
ptr = ::FFI::Pointer.new(node.address)
|
|
852
|
+
first = ::Leptris::XML::FFI.leptris_node_first_child(ptr)
|
|
853
|
+
children = []
|
|
854
|
+
current = first
|
|
855
|
+
until current.null?
|
|
856
|
+
children << ::Leptris::XML::Node.wrap(current, nil)
|
|
857
|
+
current = ::Leptris::XML::FFI.leptris_node_next_sibling(current)
|
|
858
|
+
end
|
|
859
|
+
children
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Native twin for a binding node through the document's
|
|
863
|
+
# address-keyed native cache — minting on miss so every
|
|
864
|
+
# accessor converges on one native per node (issue #219).
|
|
865
|
+
# Binding natives (Attribute/Attr, synthetic wrappers) and
|
|
866
|
+
# docless nodes pass through unchanged.
|
|
867
|
+
def canonical_native(doc, node)
|
|
868
|
+
return node unless node.is_a?(::Leptris::XML::Element)
|
|
869
|
+
|
|
870
|
+
cache = doc.native_cache
|
|
871
|
+
cache[node.c_ptr.address] ||=
|
|
872
|
+
::Leptris::XML::NativeNode.from(doc, node.c_ptr)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# The binding-node twin of bulk_native_children (identity via
|
|
876
|
+
# the binding's wrap cache).
|
|
877
|
+
def bulk_binding_children(node)
|
|
878
|
+
copied, pointers = bulk_child_buffers(node.c_ptr)
|
|
879
|
+
return node.children.to_a if copied.zero?
|
|
880
|
+
|
|
881
|
+
doc = node.document
|
|
882
|
+
Array.new(copied) do |i|
|
|
883
|
+
::Leptris::XML::Node.wrap(pointers.get_pointer(i * 8), doc)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Bulk child fetch with moxml's own grow-to-fit thread-local
|
|
888
|
+
# scratch: count first (exact), size the buffers to it, wrap
|
|
889
|
+
# each pointer as a native-layer node. Correct on every
|
|
890
|
+
# binding version — the binding's own scratch truncates at
|
|
891
|
+
# 512 on 1.9.163.x (leptris-ruby#202).
|
|
892
|
+
def bulk_native_children(node)
|
|
893
|
+
copied, pointers = bulk_child_buffers(::FFI::Pointer.new(node.address))
|
|
894
|
+
return [] if copied.zero?
|
|
895
|
+
|
|
896
|
+
doc = doc_for(node)
|
|
897
|
+
if doc.nil?
|
|
898
|
+
# Unregistered native (adapter-level use outside #root):
|
|
899
|
+
# the 163.2 binding's bulk needs a document, so walk
|
|
900
|
+
# siblings instead of crashing through a doc-less wrap.
|
|
901
|
+
return sibling_walk_children(node)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Share the layer's per-document identity cache (keyed by
|
|
905
|
+
# node address): NativeNode.from alone mints fresh objects
|
|
906
|
+
# per call on 1.9.163.x, which would fork moxml wrappers.
|
|
907
|
+
cache = doc.native_cache
|
|
908
|
+
Array.new(copied) do |i|
|
|
909
|
+
child_ptr = pointers.get_pointer(i * 8)
|
|
910
|
+
cache[child_ptr.address] ||=
|
|
911
|
+
::Leptris::XML::NativeNode.from(doc, child_ptr)
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# Shared count-then-copy fetch over moxml's grow-to-fit
|
|
916
|
+
# thread-local scratch — the layer above the binding's
|
|
917
|
+
# (buggy on 1.9.163.x) own buffers. Returns [copied, pointers].
|
|
918
|
+
def bulk_child_buffers(ptr)
|
|
919
|
+
total = ::Leptris::XML::FFI.leptris_node_children_ex(ptr, nil, nil, 0)
|
|
920
|
+
return [0, nil] if total.zero?
|
|
921
|
+
|
|
922
|
+
scratch = (Thread.current[:moxml_leptris_children] ||= {})
|
|
923
|
+
pointers = scratch[:pointers]
|
|
924
|
+
if pointers.nil? || pointers.size / 8 < total
|
|
925
|
+
pointers&.free
|
|
926
|
+
pointers = scratch[:pointers] =
|
|
927
|
+
::FFI::MemoryPointer.new(:pointer, total)
|
|
928
|
+
end
|
|
929
|
+
kinds = scratch[:kinds]
|
|
930
|
+
if kinds.nil? || kinds.size / 4 < total
|
|
931
|
+
kinds&.free
|
|
932
|
+
kinds = scratch[:kinds] = ::FFI::MemoryPointer.new(:int, total)
|
|
933
|
+
end
|
|
934
|
+
copied = ::Leptris::XML::FFI.leptris_node_children_ex(
|
|
935
|
+
ptr, pointers, kinds, total
|
|
936
|
+
)
|
|
937
|
+
[copied, pointers]
|
|
938
|
+
end
|
|
939
|
+
|
|
660
940
|
# The binding reports the root element as parentless; the
|
|
661
941
|
# moxml contract roots at the document.
|
|
662
942
|
def root_parent(node)
|
|
@@ -664,14 +944,25 @@ module Moxml
|
|
|
664
944
|
end
|
|
665
945
|
|
|
666
946
|
def next_sibling(node)
|
|
947
|
+
return node.next_sibling if NATIVE_READ_LAYER &&
|
|
948
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
949
|
+
|
|
667
950
|
node.next_sibling if node.is_a?(::Leptris::XML::Node)
|
|
668
951
|
end
|
|
669
952
|
|
|
670
953
|
def previous_sibling(node)
|
|
954
|
+
# Bridged, not the native sibling walk: the native layer
|
|
955
|
+
# exposes next_sibling only, and its children batch
|
|
956
|
+
# truncates at 512 — the predecessor beyond that would be
|
|
957
|
+
# wrong.
|
|
958
|
+
node = to_binding(node)
|
|
671
959
|
node.previous_sibling if node.is_a?(::Leptris::XML::Node)
|
|
672
960
|
end
|
|
673
961
|
|
|
674
962
|
def document(node)
|
|
963
|
+
return doc_for(node) if NATIVE_READ_LAYER &&
|
|
964
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
965
|
+
|
|
675
966
|
case node
|
|
676
967
|
when ::Leptris::XML::Document then node
|
|
677
968
|
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
|
|
@@ -680,7 +971,18 @@ module Moxml
|
|
|
680
971
|
end
|
|
681
972
|
|
|
682
973
|
def root(document)
|
|
683
|
-
document.root
|
|
974
|
+
r = document.root
|
|
975
|
+
return nil if r.nil?
|
|
976
|
+
|
|
977
|
+
if NATIVE_READ_LAYER
|
|
978
|
+
# Through the same address-keyed cache as the children
|
|
979
|
+
# path: NativeNode.from alone mints a fresh object per
|
|
980
|
+
# call on 1.9.163.x, splitting the wrappers (issue #219).
|
|
981
|
+
native = canonical_native(document, r)
|
|
982
|
+
record_native_doc(native, document)
|
|
983
|
+
return native
|
|
984
|
+
end
|
|
985
|
+
r
|
|
684
986
|
end
|
|
685
987
|
|
|
686
988
|
def line_number(node)
|
|
@@ -691,6 +993,7 @@ module Moxml
|
|
|
691
993
|
end
|
|
692
994
|
|
|
693
995
|
def attributes(element)
|
|
996
|
+
element = to_binding(element)
|
|
694
997
|
element.attribute_nodes
|
|
695
998
|
end
|
|
696
999
|
|
|
@@ -707,6 +1010,7 @@ module Moxml
|
|
|
707
1010
|
end
|
|
708
1011
|
|
|
709
1012
|
def set_attribute(element, name, value)
|
|
1013
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
710
1014
|
element[name.to_s] = value.to_s
|
|
711
1015
|
end
|
|
712
1016
|
|
|
@@ -726,14 +1030,17 @@ module Moxml
|
|
|
726
1030
|
end
|
|
727
1031
|
|
|
728
1032
|
def get_attribute(element, name)
|
|
1033
|
+
element = to_binding(element)
|
|
729
1034
|
element.attribute_nodes.find { |attr| attr.name == name.to_s }
|
|
730
1035
|
end
|
|
731
1036
|
|
|
732
1037
|
def get_attribute_value(element, name)
|
|
1038
|
+
element = to_binding(element)
|
|
733
1039
|
element[name.to_s]
|
|
734
1040
|
end
|
|
735
1041
|
|
|
736
1042
|
def remove_attribute(element, name)
|
|
1043
|
+
element = to_binding(element)
|
|
737
1044
|
element.remove_attribute(name.to_s)
|
|
738
1045
|
end
|
|
739
1046
|
|
|
@@ -743,6 +1050,10 @@ module Moxml
|
|
|
743
1050
|
end
|
|
744
1051
|
|
|
745
1052
|
def add_child(parent, child)
|
|
1053
|
+
if NATIVE_READ_LAYER
|
|
1054
|
+
parent = to_binding(parent)
|
|
1055
|
+
child = to_binding(child)
|
|
1056
|
+
end
|
|
746
1057
|
case parent
|
|
747
1058
|
when ::Leptris::XML::Document then add_document_child(parent, child)
|
|
748
1059
|
else
|
|
@@ -759,6 +1070,7 @@ module Moxml
|
|
|
759
1070
|
end
|
|
760
1071
|
|
|
761
1072
|
def add_previous_sibling(node, new_node)
|
|
1073
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
762
1074
|
# A PI inserted before the root lives at document level in
|
|
763
1075
|
# libleptris's model, not in the element tree.
|
|
764
1076
|
if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
|
|
@@ -770,10 +1082,12 @@ module Moxml
|
|
|
770
1082
|
end
|
|
771
1083
|
|
|
772
1084
|
def add_next_sibling(node, new_node)
|
|
1085
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
773
1086
|
node.add_next_sibling(new_node)
|
|
774
1087
|
end
|
|
775
1088
|
|
|
776
1089
|
def remove(node)
|
|
1090
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
777
1091
|
case node
|
|
778
1092
|
when CustomizedLeptris::Declaration
|
|
779
1093
|
remove_declaration(node.parent_doc) if node.parent_doc
|
|
@@ -793,6 +1107,10 @@ module Moxml
|
|
|
793
1107
|
end
|
|
794
1108
|
|
|
795
1109
|
def replace(node, new_node)
|
|
1110
|
+
if NATIVE_READ_LAYER
|
|
1111
|
+
node = to_binding(node)
|
|
1112
|
+
new_node = to_binding(new_node)
|
|
1113
|
+
end
|
|
796
1114
|
return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
|
|
797
1115
|
|
|
798
1116
|
# libleptris only offers element-anchored insertion, so a
|
|
@@ -817,10 +1135,20 @@ module Moxml
|
|
|
817
1135
|
end
|
|
818
1136
|
|
|
819
1137
|
def replace_children(node, new_children)
|
|
1138
|
+
if NATIVE_READ_LAYER
|
|
1139
|
+
node = to_binding(node)
|
|
1140
|
+
new_children = new_children.map { |child| to_binding(child) }
|
|
1141
|
+
end
|
|
820
1142
|
node.children = new_children
|
|
821
1143
|
end
|
|
822
1144
|
|
|
823
1145
|
def text_content(node)
|
|
1146
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
1147
|
+
return node.content if NATIVE_STRINGS_UTF8
|
|
1148
|
+
|
|
1149
|
+
return node.content.dup.force_encoding(Encoding::UTF_8)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
824
1152
|
# Frequency-ordered: elements dominate reads; they paid
|
|
825
1153
|
# three failed compares to reach the else arm. The
|
|
826
1154
|
# duplicated branch bodies are the point.
|
|
@@ -837,6 +1165,7 @@ module Moxml
|
|
|
837
1165
|
end
|
|
838
1166
|
|
|
839
1167
|
def inner_text(node)
|
|
1168
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
840
1169
|
# moxml semantic: direct text children only — no descendant
|
|
841
1170
|
# text (that is #text), no comments. Entity references
|
|
842
1171
|
# contribute their serialized form.
|
|
@@ -851,6 +1180,7 @@ module Moxml
|
|
|
851
1180
|
end
|
|
852
1181
|
|
|
853
1182
|
def set_text_content(node, content)
|
|
1183
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
854
1184
|
case node
|
|
855
1185
|
when ::Leptris::XML::Document
|
|
856
1186
|
node.root&.content = content.to_s
|
|
@@ -899,6 +1229,7 @@ module Moxml
|
|
|
899
1229
|
end
|
|
900
1230
|
|
|
901
1231
|
def namespace_definitions(element)
|
|
1232
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
902
1233
|
element.namespace_definitions
|
|
903
1234
|
end
|
|
904
1235
|
|
|
@@ -929,6 +1260,7 @@ module Moxml
|
|
|
929
1260
|
NATIVE_GATE_CACHE = XPath::Cache.new(100)
|
|
930
1261
|
|
|
931
1262
|
def xpath(node, expression, namespaces = {})
|
|
1263
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
932
1264
|
native = native_xpath(node, expression, namespaces)
|
|
933
1265
|
return native unless native.nil?
|
|
934
1266
|
|
data/lib/moxml/adapter/libxml.rb
CHANGED
data/lib/moxml/adapter/oga.rb
CHANGED
data/lib/moxml/adapter/ox.rb
CHANGED
data/lib/moxml/adapter/rexml.rb
CHANGED
data/lib/moxml/c14n.rb
CHANGED
|
@@ -66,7 +66,7 @@ module Moxml
|
|
|
66
66
|
return nil unless adapter == Moxml::Adapter::Leptris &&
|
|
67
67
|
Moxml::Adapter::Leptris.native_c14n_byte_safe?
|
|
68
68
|
|
|
69
|
-
node_or_xml.native.canonicalize(
|
|
69
|
+
Moxml::Adapter::Leptris.to_binding(node_or_xml.native).canonicalize(
|
|
70
70
|
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
71
71
|
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
72
72
|
)
|
data/lib/moxml/element.rb
CHANGED
|
@@ -62,7 +62,10 @@ module Moxml
|
|
|
62
62
|
# resolver's expanded-name semantics only pay for prefixed
|
|
63
63
|
# names, where resolution is real work.
|
|
64
64
|
adapter = self.adapter
|
|
65
|
-
|
|
65
|
+
# Capability memo: the adapter predicate is a per-read
|
|
66
|
+
# dispatch for a lifetime constant.
|
|
67
|
+
if !key.include?(":") &&
|
|
68
|
+
((@bare_qname_safe ||= adapter.bare_get_qname_safe?) == true)
|
|
66
69
|
# The entity-restore decision rides the wrapper's generation
|
|
67
70
|
# memo (as Element#text) — the adapter-level probe walks to
|
|
68
71
|
# the document and the attachment store on every read, which
|
data/lib/moxml/node.rb
CHANGED
|
@@ -54,7 +54,17 @@ module Moxml
|
|
|
54
54
|
|
|
55
55
|
def children
|
|
56
56
|
@children ||= begin
|
|
57
|
-
|
|
57
|
+
# The wrapper's entity memo decides the marker split; the
|
|
58
|
+
# adapter would otherwise re-derive it per call (a C parent
|
|
59
|
+
# climb on the native layer). The kwarg rides along only
|
|
60
|
+
# for adapters that accept it — downstream overrides with
|
|
61
|
+
# the pre-0.5.36 one-argument signature raise ArgumentError
|
|
62
|
+
# otherwise (issue #218).
|
|
63
|
+
natives = if adapter.children_accepts_entity_flag?
|
|
64
|
+
adapter.children(@native, entity_bearing: entity_bearing?)
|
|
65
|
+
else
|
|
66
|
+
adapter.children(@native)
|
|
67
|
+
end
|
|
58
68
|
natives = natives.map { adapter.patch_node(_1, @native) } if adapter.patches_children?
|
|
59
69
|
NodeSet.new(natives, context, self)
|
|
60
70
|
end
|
|
@@ -150,10 +160,24 @@ module Moxml
|
|
|
150
160
|
@entity_bearing
|
|
151
161
|
else
|
|
152
162
|
@entity_bearing_gen = gen
|
|
153
|
-
@entity_bearing = adapter.entity_bearing?(@native)
|
|
163
|
+
@entity_bearing = adapter.entity_bearing?(@native, document_native_for_markers)
|
|
154
164
|
end
|
|
155
165
|
end
|
|
156
166
|
|
|
167
|
+
# Binding document through the wrapper parent chain — pure Ruby
|
|
168
|
+
# attr reads; the native-layer fallback climbs C parents per
|
|
169
|
+
# call (the doc_for walk was the largest wrapper-layer cost on
|
|
170
|
+
# the consumer pipeline after the native adoption).
|
|
171
|
+
def document_native_for_markers
|
|
172
|
+
node = self
|
|
173
|
+
while node
|
|
174
|
+
return node.native if node.document?
|
|
175
|
+
|
|
176
|
+
node = node.parent_node
|
|
177
|
+
end
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
157
181
|
def xpath(expression, namespaces = {})
|
|
158
182
|
result = adapter.xpath(@native, expression, namespaces)
|
|
159
183
|
# Adapter contract: Array<native> | LazyNodeSet | scalar.
|
|
@@ -384,7 +408,11 @@ module Moxml
|
|
|
384
408
|
end
|
|
385
409
|
|
|
386
410
|
def ==(other)
|
|
387
|
-
|
|
411
|
+
# Native equality goes through the adapter: engines with a
|
|
412
|
+
# native read layer hand out two wrapper classes over one C
|
|
413
|
+
# node, and raw native == is false across that seam.
|
|
414
|
+
self.class == other.class &&
|
|
415
|
+
adapter.same_node?(@native, other.native)
|
|
388
416
|
end
|
|
389
417
|
|
|
390
418
|
TYPES.each do |node_type|
|
|
@@ -447,7 +475,7 @@ module Moxml
|
|
|
447
475
|
# Internal: Set the parent node for cache invalidation tracking.
|
|
448
476
|
# Called by NodeSet, Document, Element when establishing parent-child
|
|
449
477
|
# relationships. Public to allow cross-class usage within Moxml internals.
|
|
450
|
-
|
|
478
|
+
attr_accessor :parent_node
|
|
451
479
|
|
|
452
480
|
def adapter
|
|
453
481
|
# A context's adapter object is fixed for its lifetime; the
|
data/lib/moxml/version.rb
CHANGED
|
@@ -91,6 +91,16 @@
|
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
describe "document-element wrapper unification (issue #219)" do
|
|
95
|
+
it "hands the same wrapper from #root and #children in both call orders" do
|
|
96
|
+
doc = ctx.parse(%(<catalog><book/></catalog>))
|
|
97
|
+
expect(doc.root).to equal(doc.children.grep(Moxml::Element).first)
|
|
98
|
+
|
|
99
|
+
doc2 = ctx.parse(%(<catalog><book/></catalog>))
|
|
100
|
+
expect(doc2.children.grep(Moxml::Element).first).to equal(doc2.root)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
94
104
|
describe "qname create + namespace assignment (issue #208)" do
|
|
95
105
|
it "does not double the prefix when the name is already qualified" do
|
|
96
106
|
doc = ctx.parse(%(<r xmlns:p="urn:p"/>))
|
|
@@ -43,7 +43,7 @@ RSpec.shared_examples "Moxml::NodeSet" do
|
|
|
43
43
|
it "compares nodes" do
|
|
44
44
|
xpath_results = doc.xpath("//child")
|
|
45
45
|
element_children = doc.root.children.grep(Moxml::Element)
|
|
46
|
-
expect(xpath_results.
|
|
46
|
+
expect(xpath_results.to_a).to eq(element_children)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -24,4 +24,27 @@ RSpec.describe Moxml::Adapter::Base do
|
|
|
24
24
|
skip "Serialize is adapter-specific, not in Base"
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
describe "children compatibility (issue #218)" do
|
|
29
|
+
it "calls one-argument downstream overrides without the keyword" do
|
|
30
|
+
legacy = Class.new(Moxml::Adapter::Nokogiri) do
|
|
31
|
+
class << self
|
|
32
|
+
# The override exists for its one-argument signature —
|
|
33
|
+
# the pre-0.5.36 downstream shape.
|
|
34
|
+
# rubocop:disable-next Lint/UselessMethodDefinition
|
|
35
|
+
def children(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
expect(legacy.children_accepts_entity_flag?).to be(false)
|
|
41
|
+
node = Moxml.new(:nokogiri).parse(%(<r><e>x</e></r>)).root.native
|
|
42
|
+
expect(legacy.children(node).length).to eq(1)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "passes the keyword to built-in adapters" do
|
|
46
|
+
expect(Moxml::Adapter::Leptris.children_accepts_entity_flag?).to be(true)
|
|
47
|
+
expect(Moxml::Adapter::Nokogiri.children_accepts_entity_flag?).to be(true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
27
50
|
end
|
|
@@ -635,6 +635,28 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
635
635
|
end
|
|
636
636
|
end
|
|
637
637
|
|
|
638
|
+
describe "to_binding is defined regardless of the native layer (issue #217)" do
|
|
639
|
+
it "is the identity for binding nodes" do
|
|
640
|
+
ctx = Moxml.new(:leptris)
|
|
641
|
+
doc = ctx.parse(%(<r><a x="1"><b/></a></r>))
|
|
642
|
+
element = described_class.to_binding(doc.native.root)
|
|
643
|
+
expect(element).to equal(doc.native.root)
|
|
644
|
+
# every bridged entry point answers on binding natives
|
|
645
|
+
expect(doc.root.children.first.attributes.length).to eq(1)
|
|
646
|
+
expect(doc.root.children.first["x"]).to eq("1")
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
describe "materialize through the wrapper (issue #213)" do
|
|
651
|
+
it "materializes a native-layer root without NoMethodError" do
|
|
652
|
+
ctx = Moxml.new(:leptris)
|
|
653
|
+
doc = ctx.parse(%(<r><a x="1">t1</a><b><c y="2">t2</c></b></r>))
|
|
654
|
+
count = 0
|
|
655
|
+
doc.root.materialize_fields { |*_record| count += 1 }
|
|
656
|
+
expect(count).to be > 0
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
638
660
|
describe "subtree digest (issue #173, leptris#869)" do
|
|
639
661
|
let(:ctx) { Moxml.new(:leptris) }
|
|
640
662
|
|
|
@@ -79,22 +79,20 @@ RSpec.shared_examples "xml adapter" do
|
|
|
79
79
|
first = children[0]
|
|
80
80
|
second = children[1]
|
|
81
81
|
|
|
82
|
-
expect(described_class.next_sibling(first)).to
|
|
83
|
-
expect(described_class.previous_sibling(second)).to
|
|
82
|
+
expect(described_class.same_node?(described_class.next_sibling(first), second)).to be(true)
|
|
83
|
+
expect(described_class.same_node?(described_class.previous_sibling(second), first)).to be(true)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
it "adds child" do
|
|
87
87
|
element = described_class.create_element("new")
|
|
88
88
|
described_class.add_child(root, element)
|
|
89
|
-
expect(described_class.children(root).last).to
|
|
89
|
+
expect(described_class.same_node?(described_class.children(root).last, element)).to be(true)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
it "adds text child" do
|
|
93
93
|
described_class.add_child(root, "text")
|
|
94
|
-
# a workaround for Rexml until we convert tests to work with Moxml wrappers
|
|
95
94
|
last_child = described_class.children(root).last
|
|
96
|
-
|
|
97
|
-
expect(last_text).to eq("text")
|
|
95
|
+
expect(described_class.text_content(last_child)).to eq("text")
|
|
98
96
|
end
|
|
99
97
|
end
|
|
100
98
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.41
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|