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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f80e7825989e8653fcc4037f6e6a538ca28da7c6637d6bb941985bcc98d4dabf
4
- data.tar.gz: 974854c0f42279c86e404e48920a2fd6e92cebbac22a7c6781bae023fa017fde
3
+ metadata.gz: d447f74ea4b1c546e83fbb94819c6043a1fa48e0f9932ac7202009cecff63e1e
4
+ data.tar.gz: 4c33c0af41d9c628ecc993d7015d4e827d1d451b266dd7ad4145f00b322670a3
5
5
  SHA512:
6
- metadata.gz: 7749417f017a166425fb18e896e6c0346475dd78cb32792fe725153512b64c384f60cf026e5d8503e7fb0878f22a3e6f92221126f98f08550a7b58ba1e65e2a4
7
- data.tar.gz: 6545711a0dec77522f2965c5239c7cb8632878170e0fd6ea024be4d64515e49cd26550cf40b9dab7dda1ed2faa5e98caa0e076e0fda6781f7cc64463c520fd59
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 | 346ns (-18%) |
235
+ | `Element#text` read | 422ns | 249ns |
236
236
  | `create_text` allocations | 6/node | 5/node |
237
- | Bare attribute read `[]` | 714ns | 513ns (-28%) |
238
- | Consumer pipeline (parse + typed walk) | 6112µs (0.50x) | 4915µs (0.67x) |
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
@@ -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: options[: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
- def entity_bearing?(native)
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
- doc = native.is_a?(::Leptris::XML::Document) ? native : native.document
43
- # Marker-bearing text needs the split pipeline (children-level
44
- # ER expansion); the bulk path has no marker handling.
45
- return nil if doc.nil? || attachments.get(doc, :entity_markers)
46
-
47
- root_ptr = if native.is_a?(::Leptris::XML::Document)
48
- doc.root&.c_ptr
49
- else
50
- native.c_ptr
51
- end
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 = xml.include?("&") && xml.match?(RAW_AMP_RE)
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
- def self.serialize_generation
77
- @serialize_generation ||= 0
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 = serialize_generation + 1
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
 
@@ -312,7 +312,7 @@ module Moxml
312
312
  # Native documents are read-only, do nothing for them
313
313
  end
314
314
 
315
- def children(node)
315
+ def children(node, **_entity_bearing)
316
316
  native_node = unpatch_node(node)
317
317
  return [] unless native_node
318
318
 
@@ -248,7 +248,7 @@ module Moxml
248
248
  node.name = name
249
249
  end
250
250
 
251
- def children(node)
251
+ def children(node, **_entity_bearing)
252
252
  node.children
253
253
  end
254
254
 
@@ -232,7 +232,7 @@ module Moxml
232
232
  node.name = name
233
233
  end
234
234
 
235
- def children(node)
235
+ def children(node, **_entity_bearing)
236
236
  all_children = []
237
237
 
238
238
  if node.is_a?(::Oga::XML::Document)
@@ -298,7 +298,7 @@ module Moxml
298
298
  end
299
299
  end
300
300
 
301
- def children(node)
301
+ def children(node, **_entity_bearing)
302
302
  return [] unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
303
303
 
304
304
  result = node.nodes || []
@@ -188,7 +188,7 @@ module Moxml
188
188
  end
189
189
  end
190
190
 
191
- def children(node)
191
+ def children(node, **_entity_bearing)
192
192
  return [] unless node.is_a?(::REXML::Parent)
193
193
 
194
194
  # Return all children preserving whitespace text nodes
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
- if !key.include?(":") && adapter.bare_get_qname_safe?
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
- natives = adapter.children(@native)
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
- self.class == other.class && @native == other.native
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
- attr_writer :parent_node
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.35"
4
+ VERSION = "0.5.42"
5
5
  end
@@ -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.map(&:native)).to eq(element_children.map(&:native))
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 eq(second)
83
- expect(described_class.previous_sibling(second)).to eq(first)
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 eq(element)
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
- last_text = last_child.respond_to?(:text) ? last_child.text : last_child.to_s
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.35
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-14 00:00:00.000000000 Z
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