moxml 0.5.35 → 0.5.42
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 +68 -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 +370 -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: d447f74ea4b1c546e83fbb94819c6043a1fa48e0f9932ac7202009cecff63e1e
|
|
4
|
+
data.tar.gz: 4c33c0af41d9c628ecc993d7015d4e827d1d451b266dd7ad4145f00b322670a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebdc376975c3893facd06d793a04eebdfa9387814b131b9f0c5aecbd047fb57dcfadba80c40eee8ff60e52b4a7cbf1e886e67909b5f9b46623ca91b75cdb04b5
|
|
7
|
+
data.tar.gz: 9cf48a7154d6336a24504df0ec1cd62d689859f1b8f9df85e313bf69f1db97a17191e813b7f2e10bdd8565644dae88f6b323b1cd109e64995e26ea8d537bb67a
|
|
@@ -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,61 @@ 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
|
+
adopted from 1.9.163.5, where native mutations became
|
|
298
|
+
version-coherent with the binding (leptris-ruby#204/#208 — both
|
|
299
|
+
surfaces' memos drop on mutation). The earlier rejection — stale
|
|
300
|
+
binding memos plus a per-attach `to_binding` bridge costing more
|
|
301
|
+
than the factory saved (5816 -> 7697ns vs nokogiri 1750) — no
|
|
302
|
+
longer applies: `add_child` carries a both-native fast path with
|
|
303
|
+
no bridge, and `doc_for` reads the 1.9.163.5 `NativeNode#document`
|
|
304
|
+
accessor instead of the parent climb. create ×2 + attach:
|
|
305
|
+
**4846 -> 2333ns (0.30x -> 0.78x of raw nokogiri)**. Attribute
|
|
306
|
+
writes on native nodes still bridge (the native layer has no
|
|
307
|
+
`[]=`), and the native `attributes` hash (name → String) does not
|
|
308
|
+
fit the attribute-node contract — enumeration stays bridged.
|
|
309
|
+
|
|
310
|
+
Serialize (document, 30KB): passing `encoding: "UTF-8"` when it
|
|
311
|
+
equals the document's own ran a conversion pass for byte-identical
|
|
312
|
+
output (~21µs, skipped on 1.9.163.2+); the legacy
|
|
313
|
+
unescaped-ampersand probe (Linux 1.9.50 platform gem) stood down
|
|
314
|
+
below 1.9.60. 175 -> 149µs (~1.6x raw nokogiri). The dominant
|
|
315
|
+
remaining overhead is CRuby itself: a two-byte
|
|
316
|
+
`String#include?("/>")` costs ~34µs on 31KB — no memchr fast path
|
|
317
|
+
exists for multi-byte needles, and `match?` is slower still. A C
|
|
318
|
+
containment probe or a document-face `expand_empty` option would
|
|
319
|
+
close it (leptris-ruby#204, ask 4).
|
|
320
|
+
|
|
256
321
|
=== Prefer bulk paths for per-node conversion
|
|
257
322
|
|
|
258
323
|
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,119 @@ 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.5 (leptris-ruby#204/#208): native mutations are
|
|
69
|
+
# version-coherent with the binding (both surfaces' memos
|
|
70
|
+
# drop on mutation) — the builder factories and native
|
|
71
|
+
# add_child are adoptable end-to-end; NativeNode grew a
|
|
72
|
+
# document accessor and line numbers.
|
|
73
|
+
NATIVE_MUTATIONS_COHERENT =
|
|
74
|
+
defined?(::Leptris::XML::NativeNode) &&
|
|
75
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.5")
|
|
76
|
+
|
|
77
|
+
# 1.9.163.2 moved the native bulk children into C
|
|
78
|
+
# (Native.bulk_children) and fixed the 512 truncation
|
|
79
|
+
# (leptris-ruby#202). The 162.6-163.1 window carries the
|
|
80
|
+
# binding-side truncation, so moxml fetches with its own
|
|
81
|
+
# count-then-copy scratch there; newer bindings use their
|
|
82
|
+
# C bulk path (version-memoized, cheaper than a per-call
|
|
83
|
+
# count+copy).
|
|
84
|
+
NATIVE_BULK_FIXED =
|
|
85
|
+
defined?(::Leptris::XML::NativeNode) &&
|
|
86
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
|
|
87
|
+
|
|
88
|
+
# Defined unconditionally: the body self-guards, and call
|
|
89
|
+
# sites must never depend on the layer having loaded
|
|
90
|
+
# (issue #217 — binding-only installs crashed on the
|
|
91
|
+
# unguarded bridges).
|
|
92
|
+
class << self
|
|
93
|
+
# Binding node for any native: identity for binding nodes
|
|
94
|
+
# (and on installs without the native layer — the constant
|
|
95
|
+
# check short-circuits), a Node.wrap over the shared C
|
|
96
|
+
# pointer for NativeNodes.
|
|
97
|
+
def to_binding(node)
|
|
98
|
+
return node unless NATIVE_READ_LAYER &&
|
|
99
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
100
|
+
|
|
101
|
+
doc = doc_for(node)
|
|
102
|
+
ptr = ::FFI::Pointer.new(node.address)
|
|
103
|
+
return ::Leptris::XML::Node.wrap(ptr, doc) if doc
|
|
104
|
+
|
|
105
|
+
::Leptris::XML::Node.wrap(ptr, nil)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if NATIVE_READ_LAYER
|
|
110
|
+
# root NativeNode -> binding document (recorded at #root
|
|
111
|
+
# mint; the native layer exposes no document accessor).
|
|
112
|
+
@native_doc_roots = ObjectSpace::WeakMap.new
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
def record_native_doc(root_native, doc)
|
|
116
|
+
@native_doc_roots[root_native] = doc
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Protocol-level node equality: one engine can hand out
|
|
120
|
+
# two wrapper classes over the same C node (native layer +
|
|
121
|
+
# binding), so raw == is not identity across the seam.
|
|
122
|
+
def same_node?(one, other)
|
|
123
|
+
return true if one.equal?(other)
|
|
124
|
+
|
|
125
|
+
if NATIVE_READ_LAYER
|
|
126
|
+
native_one = one.is_a?(::Leptris::XML::NativeNode)
|
|
127
|
+
native_other = other.is_a?(::Leptris::XML::NativeNode)
|
|
128
|
+
if native_one || native_other
|
|
129
|
+
address_one = native_one ? one.address : one.c_ptr.address
|
|
130
|
+
address_other = native_other ? other.address : other.c_ptr.address
|
|
131
|
+
return address_one == address_other
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
one == other
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Binding document for a NativeNode subtree: climb parents
|
|
139
|
+
# to the root (C-bound reads) and look the doc up in the
|
|
140
|
+
# root registry. Detached subtrees answer nil.
|
|
141
|
+
def doc_for(node)
|
|
142
|
+
# The 1.9.163.5 native layer exposes the owning document
|
|
143
|
+
# directly; the root climb serves older bindings.
|
|
144
|
+
return node.document if NATIVE_MUTATIONS_COHERENT
|
|
145
|
+
|
|
146
|
+
current = node
|
|
147
|
+
current = current.parent while current&.parent
|
|
148
|
+
@native_doc_roots[current]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Binding node for any native: identity for binding nodes, a
|
|
154
|
+
# Node.wrap over the shared C pointer for NativeNodes (used by
|
|
155
|
+
# the write/serialize/namespace paths the native layer does
|
|
156
|
+
# not carry).
|
|
157
|
+
|
|
43
158
|
# Native C14N delegation probe: the engine's C14N must
|
|
44
159
|
# byte-match the Ruby reference (ported from canon) on a
|
|
45
160
|
# namespace-sorting + attributes + comments shape before
|
|
@@ -73,12 +188,15 @@ module Moxml
|
|
|
73
188
|
# Bumped whenever a document's :entity_markers flag is written
|
|
74
189
|
# (parse, parse_html, entity-reference mint) so wrapper-level
|
|
75
190
|
# entity_bearing? memos invalidate.
|
|
76
|
-
|
|
77
|
-
|
|
191
|
+
# attr_reader beats the ||= memo on every guarded read (the
|
|
192
|
+
# entity guard consults this per bare read / text read).
|
|
193
|
+
class << self
|
|
194
|
+
attr_reader :serialize_generation
|
|
78
195
|
end
|
|
196
|
+
@serialize_generation = 0
|
|
79
197
|
|
|
80
198
|
def self.bump_serialize_generation
|
|
81
|
-
@serialize_generation
|
|
199
|
+
@serialize_generation += 1
|
|
82
200
|
end
|
|
83
201
|
|
|
84
202
|
# leptris-ruby#103: prefixed attribute tests inside predicates
|
|
@@ -191,7 +309,14 @@ module Moxml
|
|
|
191
309
|
# document-plus-attachment probe here cost more than the
|
|
192
310
|
# read itself).
|
|
193
311
|
def bare_attr_value(element, name)
|
|
194
|
-
element[name.to_s]
|
|
312
|
+
value = element[name.to_s]
|
|
313
|
+
if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
|
|
314
|
+
element.is_a?(::Leptris::XML::NativeNode) &&
|
|
315
|
+
value.is_a?(String)
|
|
316
|
+
return value.dup.force_encoding(Encoding::UTF_8)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
value
|
|
195
320
|
end
|
|
196
321
|
|
|
197
322
|
def native_identity_stable?
|
|
@@ -365,11 +490,24 @@ module Moxml
|
|
|
365
490
|
::Leptris::XML::Document.create
|
|
366
491
|
end
|
|
367
492
|
|
|
493
|
+
# Native builder factories: one C call and a TypedData wrap.
|
|
494
|
+
# Adopted from 1.9.163.5, where native mutations became
|
|
495
|
+
# version-coherent (leptris-ruby#204/#208) — the earlier
|
|
496
|
+
# rejection (stale binding memos + per-attach bridge cost)
|
|
497
|
+
# no longer applies: add_child carries a native fast path.
|
|
368
498
|
def create_native_element(name, owner_doc = nil)
|
|
499
|
+
if NATIVE_MUTATIONS_COHERENT && owner_doc
|
|
500
|
+
return owner_doc.native_create_element(name.to_s)
|
|
501
|
+
end
|
|
502
|
+
|
|
369
503
|
(owner_doc || create_document).create_element(name.to_s)
|
|
370
504
|
end
|
|
371
505
|
|
|
372
506
|
def create_native_text(content, owner_doc = nil)
|
|
507
|
+
if NATIVE_MUTATIONS_COHERENT && owner_doc
|
|
508
|
+
return owner_doc.native_create_text(content)
|
|
509
|
+
end
|
|
510
|
+
|
|
373
511
|
(owner_doc || create_document).create_text_node(content)
|
|
374
512
|
end
|
|
375
513
|
|
|
@@ -428,10 +566,12 @@ module Moxml
|
|
|
428
566
|
end
|
|
429
567
|
|
|
430
568
|
def create_native_namespace(element, prefix, uri)
|
|
569
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
431
570
|
element.add_namespace_definition(prefix, uri)
|
|
432
571
|
end
|
|
433
572
|
|
|
434
573
|
def set_namespace(node, namespace)
|
|
574
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
435
575
|
return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
|
|
436
576
|
|
|
437
577
|
element = node
|
|
@@ -498,6 +638,8 @@ module Moxml
|
|
|
498
638
|
end
|
|
499
639
|
|
|
500
640
|
def namespace(node)
|
|
641
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
642
|
+
|
|
501
643
|
# The binding resolves Attr#namespace to the declaration but
|
|
502
644
|
# without the prefix; when the qualified name carries one,
|
|
503
645
|
# prefer the in-scope declaration that binds it so wrappers
|
|
@@ -549,6 +691,16 @@ module Moxml
|
|
|
549
691
|
end
|
|
550
692
|
|
|
551
693
|
def node_type(node)
|
|
694
|
+
if NATIVE_READ_LAYER &&
|
|
695
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
696
|
+
type = node.node_type
|
|
697
|
+
# The native layer spells PIs :pi; the wrapper contract
|
|
698
|
+
# (node_type_map) says :processing_instruction.
|
|
699
|
+
return :processing_instruction if type == :pi
|
|
700
|
+
|
|
701
|
+
return type
|
|
702
|
+
end
|
|
703
|
+
|
|
552
704
|
# Frequency-ordered: elements and text dominate every real
|
|
553
705
|
# document, and Node.wrap dispatches here once per cold
|
|
554
706
|
# wrap. CDATA must precede Text (CDATA < Text in the
|
|
@@ -574,6 +726,9 @@ module Moxml
|
|
|
574
726
|
# lightweight Attr triples answer nil.
|
|
575
727
|
def digest(node, drop_ws_text: false)
|
|
576
728
|
return nil unless DIGEST_SUPPORTED
|
|
729
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
730
|
+
return to_binding(node).digest(drop_ws: drop_ws_text)
|
|
731
|
+
end
|
|
577
732
|
return nil unless node.is_a?(::Leptris::XML::Node) &&
|
|
578
733
|
!node.is_a?(::Leptris::XML::ResultAttr)
|
|
579
734
|
|
|
@@ -584,10 +739,20 @@ module Moxml
|
|
|
584
739
|
return node.root_name if node.is_a?(::Leptris::XML::DocType)
|
|
585
740
|
return node.target if node.is_a?(CustomizedLeptris::DocumentPI)
|
|
586
741
|
|
|
742
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
743
|
+
# PIs expose no name through the native layer.
|
|
744
|
+
return to_binding(node).target.to_s if node.node_type == :pi
|
|
745
|
+
|
|
746
|
+
return node.name if NATIVE_STRINGS_UTF8
|
|
747
|
+
|
|
748
|
+
node.name.dup.force_encoding(Encoding::UTF_8)
|
|
749
|
+
end
|
|
750
|
+
|
|
587
751
|
node.name.to_s.dup.force_encoding("UTF-8")
|
|
588
752
|
end
|
|
589
753
|
|
|
590
754
|
def set_node_name(node, name)
|
|
755
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
591
756
|
case node
|
|
592
757
|
when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
|
|
593
758
|
else node.name = name
|
|
@@ -595,6 +760,7 @@ module Moxml
|
|
|
595
760
|
end
|
|
596
761
|
|
|
597
762
|
def duplicate_node(node)
|
|
763
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
598
764
|
case node
|
|
599
765
|
when CustomizedLeptris::Declaration
|
|
600
766
|
CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
|
|
@@ -609,12 +775,42 @@ module Moxml
|
|
|
609
775
|
end
|
|
610
776
|
end
|
|
611
777
|
|
|
612
|
-
def children(node)
|
|
778
|
+
def children(node, entity_bearing: false)
|
|
779
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
780
|
+
# Bulk C children through moxml's own scratch (see
|
|
781
|
+
# NATIVE_READ_LAYER note above). Entity-marker documents
|
|
782
|
+
# bridge each child for the marker split — the
|
|
783
|
+
# TextSegment reconstruction reads binding text nodes;
|
|
784
|
+
# the wrapper's memo supplies the flag, so deriving it
|
|
785
|
+
# here costs no C parent climb per call.
|
|
786
|
+
natives = if NATIVE_BULK_FIXED
|
|
787
|
+
node.children.to_a
|
|
788
|
+
else
|
|
789
|
+
bulk_native_children(node)
|
|
790
|
+
end
|
|
791
|
+
return natives unless entity_bearing
|
|
792
|
+
|
|
793
|
+
# Entity-marker documents: bridge for the marker split
|
|
794
|
+
# (TextSegment reconstruction reads binding text nodes).
|
|
795
|
+
doc = doc_for(node)
|
|
796
|
+
bound_children = natives.map { |child| to_binding(child) }
|
|
797
|
+
if doc && attachments.get(doc, :entity_markers) == false
|
|
798
|
+
return bound_children
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
return split_entity_markers(bound_children, to_binding(node))
|
|
802
|
+
end
|
|
613
803
|
# Frequency-ordered: elements dominate every walk and paid
|
|
614
804
|
# ten failed compares to reach the else arm.
|
|
615
805
|
case node
|
|
616
806
|
when ::Leptris::XML::Element
|
|
617
807
|
natives = node.children.to_a
|
|
808
|
+
if NATIVE_READ_LAYER && natives.size == 512
|
|
809
|
+
# Binding scratch truncation (leptris-ruby#202): a
|
|
810
|
+
# full batch is ambiguous — refetch through moxml's
|
|
811
|
+
# own buffers to be sure.
|
|
812
|
+
natives = bulk_binding_children(node)
|
|
813
|
+
end
|
|
618
814
|
# Parse records whether the preprocessed source held any
|
|
619
815
|
# entity markers, and the ER builder path flips the flag
|
|
620
816
|
# when it mints one. A false flag lets traversal skip the
|
|
@@ -643,6 +839,16 @@ module Moxml
|
|
|
643
839
|
end
|
|
644
840
|
|
|
645
841
|
def parent(node)
|
|
842
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
843
|
+
parent = node.parent
|
|
844
|
+
return parent if parent
|
|
845
|
+
|
|
846
|
+
doc = doc_for(node)
|
|
847
|
+
return doc if doc&.root && doc.root.c_ptr.address == node.address
|
|
848
|
+
|
|
849
|
+
return nil
|
|
850
|
+
end
|
|
851
|
+
|
|
646
852
|
# Frequency-ordered: elements dominate navigation and paid
|
|
647
853
|
# three failed compares to reach the else arm.
|
|
648
854
|
case node
|
|
@@ -657,6 +863,99 @@ module Moxml
|
|
|
657
863
|
end
|
|
658
864
|
end
|
|
659
865
|
|
|
866
|
+
# Fallback for natives without a registered document: walk
|
|
867
|
+
# first_child/next_sibling at the FFI level and wrap binding
|
|
868
|
+
# nodes over the raw pointers.
|
|
869
|
+
def sibling_walk_children(node)
|
|
870
|
+
ptr = ::FFI::Pointer.new(node.address)
|
|
871
|
+
first = ::Leptris::XML::FFI.leptris_node_first_child(ptr)
|
|
872
|
+
children = []
|
|
873
|
+
current = first
|
|
874
|
+
until current.null?
|
|
875
|
+
children << ::Leptris::XML::Node.wrap(current, nil)
|
|
876
|
+
current = ::Leptris::XML::FFI.leptris_node_next_sibling(current)
|
|
877
|
+
end
|
|
878
|
+
children
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Native twin for a binding node through the document's
|
|
882
|
+
# address-keyed native cache — minting on miss so every
|
|
883
|
+
# accessor converges on one native per node (issue #219).
|
|
884
|
+
# Binding natives (Attribute/Attr, synthetic wrappers) and
|
|
885
|
+
# docless nodes pass through unchanged.
|
|
886
|
+
def canonical_native(doc, node)
|
|
887
|
+
return node unless node.is_a?(::Leptris::XML::Element)
|
|
888
|
+
|
|
889
|
+
cache = doc.native_cache
|
|
890
|
+
cache[node.c_ptr.address] ||=
|
|
891
|
+
::Leptris::XML::NativeNode.from(doc, node.c_ptr)
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# The binding-node twin of bulk_native_children (identity via
|
|
895
|
+
# the binding's wrap cache).
|
|
896
|
+
def bulk_binding_children(node)
|
|
897
|
+
copied, pointers = bulk_child_buffers(node.c_ptr)
|
|
898
|
+
return node.children.to_a if copied.zero?
|
|
899
|
+
|
|
900
|
+
doc = node.document
|
|
901
|
+
Array.new(copied) do |i|
|
|
902
|
+
::Leptris::XML::Node.wrap(pointers.get_pointer(i * 8), doc)
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# Bulk child fetch with moxml's own grow-to-fit thread-local
|
|
907
|
+
# scratch: count first (exact), size the buffers to it, wrap
|
|
908
|
+
# each pointer as a native-layer node. Correct on every
|
|
909
|
+
# binding version — the binding's own scratch truncates at
|
|
910
|
+
# 512 on 1.9.163.x (leptris-ruby#202).
|
|
911
|
+
def bulk_native_children(node)
|
|
912
|
+
copied, pointers = bulk_child_buffers(::FFI::Pointer.new(node.address))
|
|
913
|
+
return [] if copied.zero?
|
|
914
|
+
|
|
915
|
+
doc = doc_for(node)
|
|
916
|
+
if doc.nil?
|
|
917
|
+
# Unregistered native (adapter-level use outside #root):
|
|
918
|
+
# the 163.2 binding's bulk needs a document, so walk
|
|
919
|
+
# siblings instead of crashing through a doc-less wrap.
|
|
920
|
+
return sibling_walk_children(node)
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Share the layer's per-document identity cache (keyed by
|
|
924
|
+
# node address): NativeNode.from alone mints fresh objects
|
|
925
|
+
# per call on 1.9.163.x, which would fork moxml wrappers.
|
|
926
|
+
cache = doc.native_cache
|
|
927
|
+
Array.new(copied) do |i|
|
|
928
|
+
child_ptr = pointers.get_pointer(i * 8)
|
|
929
|
+
cache[child_ptr.address] ||=
|
|
930
|
+
::Leptris::XML::NativeNode.from(doc, child_ptr)
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Shared count-then-copy fetch over moxml's grow-to-fit
|
|
935
|
+
# thread-local scratch — the layer above the binding's
|
|
936
|
+
# (buggy on 1.9.163.x) own buffers. Returns [copied, pointers].
|
|
937
|
+
def bulk_child_buffers(ptr)
|
|
938
|
+
total = ::Leptris::XML::FFI.leptris_node_children_ex(ptr, nil, nil, 0)
|
|
939
|
+
return [0, nil] if total.zero?
|
|
940
|
+
|
|
941
|
+
scratch = (Thread.current[:moxml_leptris_children] ||= {})
|
|
942
|
+
pointers = scratch[:pointers]
|
|
943
|
+
if pointers.nil? || pointers.size / 8 < total
|
|
944
|
+
pointers&.free
|
|
945
|
+
pointers = scratch[:pointers] =
|
|
946
|
+
::FFI::MemoryPointer.new(:pointer, total)
|
|
947
|
+
end
|
|
948
|
+
kinds = scratch[:kinds]
|
|
949
|
+
if kinds.nil? || kinds.size / 4 < total
|
|
950
|
+
kinds&.free
|
|
951
|
+
kinds = scratch[:kinds] = ::FFI::MemoryPointer.new(:int, total)
|
|
952
|
+
end
|
|
953
|
+
copied = ::Leptris::XML::FFI.leptris_node_children_ex(
|
|
954
|
+
ptr, pointers, kinds, total
|
|
955
|
+
)
|
|
956
|
+
[copied, pointers]
|
|
957
|
+
end
|
|
958
|
+
|
|
660
959
|
# The binding reports the root element as parentless; the
|
|
661
960
|
# moxml contract roots at the document.
|
|
662
961
|
def root_parent(node)
|
|
@@ -664,14 +963,25 @@ module Moxml
|
|
|
664
963
|
end
|
|
665
964
|
|
|
666
965
|
def next_sibling(node)
|
|
966
|
+
return node.next_sibling if NATIVE_READ_LAYER &&
|
|
967
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
968
|
+
|
|
667
969
|
node.next_sibling if node.is_a?(::Leptris::XML::Node)
|
|
668
970
|
end
|
|
669
971
|
|
|
670
972
|
def previous_sibling(node)
|
|
973
|
+
# Bridged, not the native sibling walk: the native layer
|
|
974
|
+
# exposes next_sibling only, and its children batch
|
|
975
|
+
# truncates at 512 — the predecessor beyond that would be
|
|
976
|
+
# wrong.
|
|
977
|
+
node = to_binding(node)
|
|
671
978
|
node.previous_sibling if node.is_a?(::Leptris::XML::Node)
|
|
672
979
|
end
|
|
673
980
|
|
|
674
981
|
def document(node)
|
|
982
|
+
return doc_for(node) if NATIVE_READ_LAYER &&
|
|
983
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
984
|
+
|
|
675
985
|
case node
|
|
676
986
|
when ::Leptris::XML::Document then node
|
|
677
987
|
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
|
|
@@ -680,10 +990,26 @@ module Moxml
|
|
|
680
990
|
end
|
|
681
991
|
|
|
682
992
|
def root(document)
|
|
683
|
-
document.root
|
|
993
|
+
r = document.root
|
|
994
|
+
return nil if r.nil?
|
|
995
|
+
|
|
996
|
+
if NATIVE_READ_LAYER
|
|
997
|
+
# Through the same address-keyed cache as the children
|
|
998
|
+
# path: NativeNode.from alone mints a fresh object per
|
|
999
|
+
# call on 1.9.163.x, splitting the wrappers (issue #219).
|
|
1000
|
+
native = canonical_native(document, r)
|
|
1001
|
+
record_native_doc(native, document)
|
|
1002
|
+
return native
|
|
1003
|
+
end
|
|
1004
|
+
r
|
|
684
1005
|
end
|
|
685
1006
|
|
|
686
1007
|
def line_number(node)
|
|
1008
|
+
if NATIVE_MUTATIONS_COHERENT &&
|
|
1009
|
+
node.is_a?(::Leptris::XML::NativeNode)
|
|
1010
|
+
line = node.line
|
|
1011
|
+
return line.nil? || line.zero? ? nil : line
|
|
1012
|
+
end
|
|
687
1013
|
return nil unless node.is_a?(::Leptris::XML::Node)
|
|
688
1014
|
|
|
689
1015
|
line = node.line
|
|
@@ -691,6 +1017,7 @@ module Moxml
|
|
|
691
1017
|
end
|
|
692
1018
|
|
|
693
1019
|
def attributes(element)
|
|
1020
|
+
element = to_binding(element)
|
|
694
1021
|
element.attribute_nodes
|
|
695
1022
|
end
|
|
696
1023
|
|
|
@@ -707,6 +1034,7 @@ module Moxml
|
|
|
707
1034
|
end
|
|
708
1035
|
|
|
709
1036
|
def set_attribute(element, name, value)
|
|
1037
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
710
1038
|
element[name.to_s] = value.to_s
|
|
711
1039
|
end
|
|
712
1040
|
|
|
@@ -726,14 +1054,17 @@ module Moxml
|
|
|
726
1054
|
end
|
|
727
1055
|
|
|
728
1056
|
def get_attribute(element, name)
|
|
1057
|
+
element = to_binding(element)
|
|
729
1058
|
element.attribute_nodes.find { |attr| attr.name == name.to_s }
|
|
730
1059
|
end
|
|
731
1060
|
|
|
732
1061
|
def get_attribute_value(element, name)
|
|
1062
|
+
element = to_binding(element)
|
|
733
1063
|
element[name.to_s]
|
|
734
1064
|
end
|
|
735
1065
|
|
|
736
1066
|
def remove_attribute(element, name)
|
|
1067
|
+
element = to_binding(element)
|
|
737
1068
|
element.remove_attribute(name.to_s)
|
|
738
1069
|
end
|
|
739
1070
|
|
|
@@ -743,6 +1074,12 @@ module Moxml
|
|
|
743
1074
|
end
|
|
744
1075
|
|
|
745
1076
|
def add_child(parent, child)
|
|
1077
|
+
if NATIVE_READ_LAYER && !(NATIVE_MUTATIONS_COHERENT &&
|
|
1078
|
+
parent.is_a?(::Leptris::XML::NativeNode) &&
|
|
1079
|
+
child.is_a?(::Leptris::XML::NativeNode))
|
|
1080
|
+
parent = to_binding(parent)
|
|
1081
|
+
child = to_binding(child)
|
|
1082
|
+
end
|
|
746
1083
|
case parent
|
|
747
1084
|
when ::Leptris::XML::Document then add_document_child(parent, child)
|
|
748
1085
|
else
|
|
@@ -759,6 +1096,10 @@ module Moxml
|
|
|
759
1096
|
end
|
|
760
1097
|
|
|
761
1098
|
def add_previous_sibling(node, new_node)
|
|
1099
|
+
if NATIVE_READ_LAYER
|
|
1100
|
+
node = to_binding(node)
|
|
1101
|
+
new_node = to_binding(new_node)
|
|
1102
|
+
end
|
|
762
1103
|
# A PI inserted before the root lives at document level in
|
|
763
1104
|
# libleptris's model, not in the element tree.
|
|
764
1105
|
if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
|
|
@@ -770,10 +1111,15 @@ module Moxml
|
|
|
770
1111
|
end
|
|
771
1112
|
|
|
772
1113
|
def add_next_sibling(node, new_node)
|
|
1114
|
+
if NATIVE_READ_LAYER
|
|
1115
|
+
node = to_binding(node)
|
|
1116
|
+
new_node = to_binding(new_node)
|
|
1117
|
+
end
|
|
773
1118
|
node.add_next_sibling(new_node)
|
|
774
1119
|
end
|
|
775
1120
|
|
|
776
1121
|
def remove(node)
|
|
1122
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
777
1123
|
case node
|
|
778
1124
|
when CustomizedLeptris::Declaration
|
|
779
1125
|
remove_declaration(node.parent_doc) if node.parent_doc
|
|
@@ -793,6 +1139,10 @@ module Moxml
|
|
|
793
1139
|
end
|
|
794
1140
|
|
|
795
1141
|
def replace(node, new_node)
|
|
1142
|
+
if NATIVE_READ_LAYER
|
|
1143
|
+
node = to_binding(node)
|
|
1144
|
+
new_node = to_binding(new_node)
|
|
1145
|
+
end
|
|
796
1146
|
return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
|
|
797
1147
|
|
|
798
1148
|
# libleptris only offers element-anchored insertion, so a
|
|
@@ -817,10 +1167,20 @@ module Moxml
|
|
|
817
1167
|
end
|
|
818
1168
|
|
|
819
1169
|
def replace_children(node, new_children)
|
|
1170
|
+
if NATIVE_READ_LAYER
|
|
1171
|
+
node = to_binding(node)
|
|
1172
|
+
new_children = new_children.map { |child| to_binding(child) }
|
|
1173
|
+
end
|
|
820
1174
|
node.children = new_children
|
|
821
1175
|
end
|
|
822
1176
|
|
|
823
1177
|
def text_content(node)
|
|
1178
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
1179
|
+
return node.content if NATIVE_STRINGS_UTF8
|
|
1180
|
+
|
|
1181
|
+
return node.content.dup.force_encoding(Encoding::UTF_8)
|
|
1182
|
+
end
|
|
1183
|
+
|
|
824
1184
|
# Frequency-ordered: elements dominate reads; they paid
|
|
825
1185
|
# three failed compares to reach the else arm. The
|
|
826
1186
|
# duplicated branch bodies are the point.
|
|
@@ -837,6 +1197,7 @@ module Moxml
|
|
|
837
1197
|
end
|
|
838
1198
|
|
|
839
1199
|
def inner_text(node)
|
|
1200
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
840
1201
|
# moxml semantic: direct text children only — no descendant
|
|
841
1202
|
# text (that is #text), no comments. Entity references
|
|
842
1203
|
# contribute their serialized form.
|
|
@@ -851,6 +1212,7 @@ module Moxml
|
|
|
851
1212
|
end
|
|
852
1213
|
|
|
853
1214
|
def set_text_content(node, content)
|
|
1215
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
854
1216
|
case node
|
|
855
1217
|
when ::Leptris::XML::Document
|
|
856
1218
|
node.root&.content = content.to_s
|
|
@@ -899,6 +1261,7 @@ module Moxml
|
|
|
899
1261
|
end
|
|
900
1262
|
|
|
901
1263
|
def namespace_definitions(element)
|
|
1264
|
+
element = to_binding(element) if NATIVE_READ_LAYER
|
|
902
1265
|
element.namespace_definitions
|
|
903
1266
|
end
|
|
904
1267
|
|
|
@@ -929,6 +1292,7 @@ module Moxml
|
|
|
929
1292
|
NATIVE_GATE_CACHE = XPath::Cache.new(100)
|
|
930
1293
|
|
|
931
1294
|
def xpath(node, expression, namespaces = {})
|
|
1295
|
+
node = to_binding(node) if NATIVE_READ_LAYER
|
|
932
1296
|
native = native_xpath(node, expression, namespaces)
|
|
933
1297
|
return native unless native.nil?
|
|
934
1298
|
|
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.42
|
|
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
|