moxml 0.5.35 → 0.5.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f80e7825989e8653fcc4037f6e6a538ca28da7c6637d6bb941985bcc98d4dabf
4
- data.tar.gz: 974854c0f42279c86e404e48920a2fd6e92cebbac22a7c6781bae023fa017fde
3
+ metadata.gz: d96e7e77957533fdb65727e5d244d038a6947b30a46563463b995114e4594faf
4
+ data.tar.gz: 11940d59f145433f37e0a5ea286055cf6d30167614291d007ff2d41dc68606a5
5
5
  SHA512:
6
- metadata.gz: 7749417f017a166425fb18e896e6c0346475dd78cb32792fe725153512b64c384f60cf026e5d8503e7fb0878f22a3e6f92221126f98f08550a7b58ba1e65e2a4
7
- data.tar.gz: 6545711a0dec77522f2965c5239c7cb8632878170e0fd6ea024be4d64515e49cd26550cf40b9dab7dda1ed2faa5e98caa0e076e0fda6781f7cc64463c520fd59
6
+ metadata.gz: 25d5f4a19555bd8802f978a3586d2d4c7fc9770d918fbfee1777203d548c6ed8d0d67fa42fd4dcddc047675f2034482f1323542379fe61193358209a4f8670d0
7
+ data.tar.gz: be203488271f744cb405f70a48af237408870c433a7b5922c8cafb1f303b2a4a451d0b0f6c3c80ce249a9fe75ec98904c330ff3a0cda212587fb231aac255f53
@@ -232,10 +232,20 @@ generation; `xmlns` writes keep the global bump.
232
232
  | Attribute write (`[]=`) | 822ns | 758ns (-8%) |
233
233
  | Cold walk (parse + wrapper mint, 752 nodes) | 811µs | 699µs (-14%) |
234
234
  | Mixed build+read (nokogiri) | 1355µs / 5267 allocs | 1283µs / 5267 allocs |
235
- | `Element#text` read | 422ns | 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,54 @@ read cache materializes lazily). The remaining cold-walk gap is
253
263
  binding-side (its per-node Ruby wrapper construction) — tracked
254
264
  upstream as the TypedData work.
255
265
 
266
+ === Native read layer (leptris >= 1.9.162.6)
267
+
268
+ The leptris adapter adopts the binding's opt-in native read layer
269
+ (leptris-ruby#185): TypedData node wrappers with C-bound hot reads
270
+ and bulk children construction — one wrapper-cache round-trip for a
271
+ whole child list, zero per-node FFI. Element natives minted from
272
+ `#root` and `children` are native-layer nodes; the C tree is shared
273
+ with the binding, so writes made through the bridged binding nodes
274
+ (a `to_binding` wrap over the same pointer) are visible to native
275
+ reads. Engines without the layer, or ruby-platform installs without
276
+ the compiled bundle, stay on the binding path unchanged.
277
+
278
+ | path | binding path | native layer |
279
+ | Consumer pipeline (parse + typed walk) | 6112µs (0.50x nokogiri) | **3380µs (1.00-1.04x)** |
280
+ | Pipeline allocations | 13564 | **9813 (0.69x nokogiri)** |
281
+ | Cold walk (parse + mint) | 738µs | **509µs** |
282
+ | Raw walk (binding vs native, upstream's fixture) | 64.3µs/doc | 22.8µs/doc |
283
+
284
+ Entity-marker documents fall back to the binding children path (the
285
+ marker split materializes TextSegments); the wrapper's
286
+ `entity_bearing?` memo decides, so the check costs no per-call
287
+ climb. `Node#==` and the adapter protocol's `same_node?` compare
288
+ C-node addresses — two wrapper classes over one node compare equal.
289
+
290
+ The 1.9.162.6–163.1 bindings truncate bulk children at 512
291
+ (leptris-ruby#202): moxml fetches with its own count-then-copy
292
+ scratch there. 1.9.163.2 moved the bulk into C and fixed the
293
+ truncation — newer bindings use their C path (version-memoized,
294
+ cheaper than a per-call count+copy).
295
+
296
+ The native *builder* factories (native_create_element/text) are
297
+ deliberately not adopted: native mutations do not advance the
298
+ binding's document version, so the binding's memoized reads over
299
+ the same tree go stale, and attaching the created natives pays a
300
+ per-node `to_binding` bridge that costs more than the factory
301
+ saves (create+attach measured 5816 -> 7697ns vs nokogiri 1750).
302
+
303
+ Serialize (document, 30KB): passing `encoding: "UTF-8"` when it
304
+ equals the document's own ran a conversion pass for byte-identical
305
+ output (~21µs, skipped on 1.9.163.2+); the legacy
306
+ unescaped-ampersand probe (Linux 1.9.50 platform gem) stood down
307
+ below 1.9.60. 175 -> 149µs (~1.6x raw nokogiri). The dominant
308
+ remaining overhead is CRuby itself: a two-byte
309
+ `String#include?("/>")` costs ~34µs on 31KB — no memchr fast path
310
+ exists for multi-byte needles, and `match?` is slower still. A C
311
+ containment probe or a document-face `expand_empty` option would
312
+ close it (leptris-ruby#204, ask 4).
313
+
256
314
  === Prefer bulk paths for per-node conversion
257
315
 
258
316
  Per-node Ruby iteration pays the wrapper tax per element. When the
@@ -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,106 @@ module Moxml
40
42
  DIGEST_SUPPORTED =
41
43
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
42
44
 
45
+ # Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
46
+ # TypedData node wrappers with C-bound hot reads and bulk
47
+ # children construction. Minted from #root downward; the C
48
+ # tree is shared with the binding, so writes made through the
49
+ # bridged binding nodes are visible to native reads. Installs
50
+ # without the compiled bundle (ruby-platform gem) simply stay
51
+ # on the binding path.
52
+ begin
53
+ require "leptris/xml/native_layer"
54
+ rescue LoadError, StandardError
55
+ nil
56
+ end
57
+ NATIVE_READ_LAYER =
58
+ defined?(::Leptris::XML::NativeNode) &&
59
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
60
+
61
+ # 1.9.163.2's native layer returns UTF-8, unfrozen strings
62
+ # (1.9.162.x answered ASCII-8BIT — the reads retagged with a
63
+ # dup+force_encoding per call).
64
+ NATIVE_STRINGS_UTF8 =
65
+ defined?(::Leptris::XML::NativeNode) &&
66
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
67
+
68
+ # 1.9.163.2 moved the native bulk children into C
69
+ # (Native.bulk_children) and fixed the 512 truncation
70
+ # (leptris-ruby#202). The 162.6-163.1 window carries the
71
+ # binding-side truncation, so moxml fetches with its own
72
+ # count-then-copy scratch there; newer bindings use their
73
+ # C bulk path (version-memoized, cheaper than a per-call
74
+ # count+copy).
75
+ NATIVE_BULK_FIXED =
76
+ defined?(::Leptris::XML::NativeNode) &&
77
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
78
+
79
+ # Defined unconditionally: the body self-guards, and call
80
+ # sites must never depend on the layer having loaded
81
+ # (issue #217 — binding-only installs crashed on the
82
+ # unguarded bridges).
83
+ class << self
84
+ # Binding node for any native: identity for binding nodes
85
+ # (and on installs without the native layer — the constant
86
+ # check short-circuits), a Node.wrap over the shared C
87
+ # pointer for NativeNodes.
88
+ def to_binding(node)
89
+ return node unless NATIVE_READ_LAYER &&
90
+ node.is_a?(::Leptris::XML::NativeNode)
91
+
92
+ doc = doc_for(node)
93
+ ptr = ::FFI::Pointer.new(node.address)
94
+ return ::Leptris::XML::Node.wrap(ptr, doc) if doc
95
+
96
+ ::Leptris::XML::Node.wrap(ptr, nil)
97
+ end
98
+ end
99
+
100
+ if NATIVE_READ_LAYER
101
+ # root NativeNode -> binding document (recorded at #root
102
+ # mint; the native layer exposes no document accessor).
103
+ @native_doc_roots = ObjectSpace::WeakMap.new
104
+
105
+ class << self
106
+ def record_native_doc(root_native, doc)
107
+ @native_doc_roots[root_native] = doc
108
+ end
109
+
110
+ # Protocol-level node equality: one engine can hand out
111
+ # two wrapper classes over the same C node (native layer +
112
+ # binding), so raw == is not identity across the seam.
113
+ def same_node?(one, other)
114
+ return true if one.equal?(other)
115
+
116
+ if NATIVE_READ_LAYER
117
+ native_one = one.is_a?(::Leptris::XML::NativeNode)
118
+ native_other = other.is_a?(::Leptris::XML::NativeNode)
119
+ if native_one || native_other
120
+ address_one = native_one ? one.address : one.c_ptr.address
121
+ address_other = native_other ? other.address : other.c_ptr.address
122
+ return address_one == address_other
123
+ end
124
+ end
125
+
126
+ one == other
127
+ end
128
+
129
+ # Binding document for a NativeNode subtree: climb parents
130
+ # to the root (C-bound reads) and look the doc up in the
131
+ # root registry. Detached subtrees answer nil.
132
+ def doc_for(node)
133
+ current = node
134
+ current = current.parent while current&.parent
135
+ @native_doc_roots[current]
136
+ end
137
+ end
138
+ end
139
+
140
+ # Binding node for any native: identity for binding nodes, a
141
+ # Node.wrap over the shared C pointer for NativeNodes (used by
142
+ # the write/serialize/namespace paths the native layer does
143
+ # not carry).
144
+
43
145
  # Native C14N delegation probe: the engine's C14N must
44
146
  # byte-match the Ruby reference (ported from canon) on a
45
147
  # namespace-sorting + attributes + comments shape before
@@ -73,12 +175,15 @@ module Moxml
73
175
  # Bumped whenever a document's :entity_markers flag is written
74
176
  # (parse, parse_html, entity-reference mint) so wrapper-level
75
177
  # entity_bearing? memos invalidate.
76
- def self.serialize_generation
77
- @serialize_generation ||= 0
178
+ # attr_reader beats the ||= memo on every guarded read (the
179
+ # entity guard consults this per bare read / text read).
180
+ class << self
181
+ attr_reader :serialize_generation
78
182
  end
183
+ @serialize_generation = 0
79
184
 
80
185
  def self.bump_serialize_generation
81
- @serialize_generation = serialize_generation + 1
186
+ @serialize_generation += 1
82
187
  end
83
188
 
84
189
  # leptris-ruby#103: prefixed attribute tests inside predicates
@@ -191,7 +296,14 @@ module Moxml
191
296
  # document-plus-attachment probe here cost more than the
192
297
  # read itself).
193
298
  def bare_attr_value(element, name)
194
- element[name.to_s]
299
+ value = element[name.to_s]
300
+ if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
301
+ element.is_a?(::Leptris::XML::NativeNode) &&
302
+ value.is_a?(String)
303
+ return value.dup.force_encoding(Encoding::UTF_8)
304
+ end
305
+
306
+ value
195
307
  end
196
308
 
197
309
  def native_identity_stable?
@@ -365,6 +477,13 @@ module Moxml
365
477
  ::Leptris::XML::Document.create
366
478
  end
367
479
 
480
+ # The native builder factories (native_create_element/text)
481
+ # are deliberately NOT adopted: native mutations do not
482
+ # advance the binding's document version, so the binding's
483
+ # memoized reads over the same tree go stale; attaching the
484
+ # created natives also pays a per-node to_binding bridge
485
+ # that costs more than the factory saves (create+attach
486
+ # measured 5816 -> 7697ns vs nokogiri 1750).
368
487
  def create_native_element(name, owner_doc = nil)
369
488
  (owner_doc || create_document).create_element(name.to_s)
370
489
  end
@@ -428,10 +547,12 @@ module Moxml
428
547
  end
429
548
 
430
549
  def create_native_namespace(element, prefix, uri)
550
+ element = to_binding(element) if NATIVE_READ_LAYER
431
551
  element.add_namespace_definition(prefix, uri)
432
552
  end
433
553
 
434
554
  def set_namespace(node, namespace)
555
+ node = to_binding(node) if NATIVE_READ_LAYER
435
556
  return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
436
557
 
437
558
  element = node
@@ -498,6 +619,8 @@ module Moxml
498
619
  end
499
620
 
500
621
  def namespace(node)
622
+ node = to_binding(node) if NATIVE_READ_LAYER
623
+
501
624
  # The binding resolves Attr#namespace to the declaration but
502
625
  # without the prefix; when the qualified name carries one,
503
626
  # prefer the in-scope declaration that binds it so wrappers
@@ -549,6 +672,16 @@ module Moxml
549
672
  end
550
673
 
551
674
  def node_type(node)
675
+ if NATIVE_READ_LAYER &&
676
+ node.is_a?(::Leptris::XML::NativeNode)
677
+ type = node.node_type
678
+ # The native layer spells PIs :pi; the wrapper contract
679
+ # (node_type_map) says :processing_instruction.
680
+ return :processing_instruction if type == :pi
681
+
682
+ return type
683
+ end
684
+
552
685
  # Frequency-ordered: elements and text dominate every real
553
686
  # document, and Node.wrap dispatches here once per cold
554
687
  # wrap. CDATA must precede Text (CDATA < Text in the
@@ -574,6 +707,9 @@ module Moxml
574
707
  # lightweight Attr triples answer nil.
575
708
  def digest(node, drop_ws_text: false)
576
709
  return nil unless DIGEST_SUPPORTED
710
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
711
+ return to_binding(node).digest(drop_ws: drop_ws_text)
712
+ end
577
713
  return nil unless node.is_a?(::Leptris::XML::Node) &&
578
714
  !node.is_a?(::Leptris::XML::ResultAttr)
579
715
 
@@ -584,10 +720,20 @@ module Moxml
584
720
  return node.root_name if node.is_a?(::Leptris::XML::DocType)
585
721
  return node.target if node.is_a?(CustomizedLeptris::DocumentPI)
586
722
 
723
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
724
+ # PIs expose no name through the native layer.
725
+ return to_binding(node).target.to_s if node.node_type == :pi
726
+
727
+ return node.name if NATIVE_STRINGS_UTF8
728
+
729
+ node.name.dup.force_encoding(Encoding::UTF_8)
730
+ end
731
+
587
732
  node.name.to_s.dup.force_encoding("UTF-8")
588
733
  end
589
734
 
590
735
  def set_node_name(node, name)
736
+ node = to_binding(node) if NATIVE_READ_LAYER
591
737
  case node
592
738
  when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
593
739
  else node.name = name
@@ -595,6 +741,7 @@ module Moxml
595
741
  end
596
742
 
597
743
  def duplicate_node(node)
744
+ node = to_binding(node) if NATIVE_READ_LAYER
598
745
  case node
599
746
  when CustomizedLeptris::Declaration
600
747
  CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
@@ -609,12 +756,42 @@ module Moxml
609
756
  end
610
757
  end
611
758
 
612
- def children(node)
759
+ def children(node, entity_bearing: false)
760
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
761
+ # Bulk C children through moxml's own scratch (see
762
+ # NATIVE_READ_LAYER note above). Entity-marker documents
763
+ # bridge each child for the marker split — the
764
+ # TextSegment reconstruction reads binding text nodes;
765
+ # the wrapper's memo supplies the flag, so deriving it
766
+ # here costs no C parent climb per call.
767
+ natives = if NATIVE_BULK_FIXED
768
+ node.children.to_a
769
+ else
770
+ bulk_native_children(node)
771
+ end
772
+ return natives unless entity_bearing
773
+
774
+ # Entity-marker documents: bridge for the marker split
775
+ # (TextSegment reconstruction reads binding text nodes).
776
+ doc = doc_for(node)
777
+ bound_children = natives.map { |child| to_binding(child) }
778
+ if doc && attachments.get(doc, :entity_markers) == false
779
+ return bound_children
780
+ end
781
+
782
+ return split_entity_markers(bound_children, to_binding(node))
783
+ end
613
784
  # Frequency-ordered: elements dominate every walk and paid
614
785
  # ten failed compares to reach the else arm.
615
786
  case node
616
787
  when ::Leptris::XML::Element
617
788
  natives = node.children.to_a
789
+ if NATIVE_READ_LAYER && natives.size == 512
790
+ # Binding scratch truncation (leptris-ruby#202): a
791
+ # full batch is ambiguous — refetch through moxml's
792
+ # own buffers to be sure.
793
+ natives = bulk_binding_children(node)
794
+ end
618
795
  # Parse records whether the preprocessed source held any
619
796
  # entity markers, and the ER builder path flips the flag
620
797
  # when it mints one. A false flag lets traversal skip the
@@ -643,6 +820,16 @@ module Moxml
643
820
  end
644
821
 
645
822
  def parent(node)
823
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
824
+ parent = node.parent
825
+ return parent if parent
826
+
827
+ doc = doc_for(node)
828
+ return doc if doc&.root && doc.root.c_ptr.address == node.address
829
+
830
+ return nil
831
+ end
832
+
646
833
  # Frequency-ordered: elements dominate navigation and paid
647
834
  # three failed compares to reach the else arm.
648
835
  case node
@@ -657,6 +844,99 @@ module Moxml
657
844
  end
658
845
  end
659
846
 
847
+ # Fallback for natives without a registered document: walk
848
+ # first_child/next_sibling at the FFI level and wrap binding
849
+ # nodes over the raw pointers.
850
+ def sibling_walk_children(node)
851
+ ptr = ::FFI::Pointer.new(node.address)
852
+ first = ::Leptris::XML::FFI.leptris_node_first_child(ptr)
853
+ children = []
854
+ current = first
855
+ until current.null?
856
+ children << ::Leptris::XML::Node.wrap(current, nil)
857
+ current = ::Leptris::XML::FFI.leptris_node_next_sibling(current)
858
+ end
859
+ children
860
+ end
861
+
862
+ # Native twin for a binding node through the document's
863
+ # address-keyed native cache — minting on miss so every
864
+ # accessor converges on one native per node (issue #219).
865
+ # Binding natives (Attribute/Attr, synthetic wrappers) and
866
+ # docless nodes pass through unchanged.
867
+ def canonical_native(doc, node)
868
+ return node unless node.is_a?(::Leptris::XML::Element)
869
+
870
+ cache = doc.native_cache
871
+ cache[node.c_ptr.address] ||=
872
+ ::Leptris::XML::NativeNode.from(doc, node.c_ptr)
873
+ end
874
+
875
+ # The binding-node twin of bulk_native_children (identity via
876
+ # the binding's wrap cache).
877
+ def bulk_binding_children(node)
878
+ copied, pointers = bulk_child_buffers(node.c_ptr)
879
+ return node.children.to_a if copied.zero?
880
+
881
+ doc = node.document
882
+ Array.new(copied) do |i|
883
+ ::Leptris::XML::Node.wrap(pointers.get_pointer(i * 8), doc)
884
+ end
885
+ end
886
+
887
+ # Bulk child fetch with moxml's own grow-to-fit thread-local
888
+ # scratch: count first (exact), size the buffers to it, wrap
889
+ # each pointer as a native-layer node. Correct on every
890
+ # binding version — the binding's own scratch truncates at
891
+ # 512 on 1.9.163.x (leptris-ruby#202).
892
+ def bulk_native_children(node)
893
+ copied, pointers = bulk_child_buffers(::FFI::Pointer.new(node.address))
894
+ return [] if copied.zero?
895
+
896
+ doc = doc_for(node)
897
+ if doc.nil?
898
+ # Unregistered native (adapter-level use outside #root):
899
+ # the 163.2 binding's bulk needs a document, so walk
900
+ # siblings instead of crashing through a doc-less wrap.
901
+ return sibling_walk_children(node)
902
+ end
903
+
904
+ # Share the layer's per-document identity cache (keyed by
905
+ # node address): NativeNode.from alone mints fresh objects
906
+ # per call on 1.9.163.x, which would fork moxml wrappers.
907
+ cache = doc.native_cache
908
+ Array.new(copied) do |i|
909
+ child_ptr = pointers.get_pointer(i * 8)
910
+ cache[child_ptr.address] ||=
911
+ ::Leptris::XML::NativeNode.from(doc, child_ptr)
912
+ end
913
+ end
914
+
915
+ # Shared count-then-copy fetch over moxml's grow-to-fit
916
+ # thread-local scratch — the layer above the binding's
917
+ # (buggy on 1.9.163.x) own buffers. Returns [copied, pointers].
918
+ def bulk_child_buffers(ptr)
919
+ total = ::Leptris::XML::FFI.leptris_node_children_ex(ptr, nil, nil, 0)
920
+ return [0, nil] if total.zero?
921
+
922
+ scratch = (Thread.current[:moxml_leptris_children] ||= {})
923
+ pointers = scratch[:pointers]
924
+ if pointers.nil? || pointers.size / 8 < total
925
+ pointers&.free
926
+ pointers = scratch[:pointers] =
927
+ ::FFI::MemoryPointer.new(:pointer, total)
928
+ end
929
+ kinds = scratch[:kinds]
930
+ if kinds.nil? || kinds.size / 4 < total
931
+ kinds&.free
932
+ kinds = scratch[:kinds] = ::FFI::MemoryPointer.new(:int, total)
933
+ end
934
+ copied = ::Leptris::XML::FFI.leptris_node_children_ex(
935
+ ptr, pointers, kinds, total
936
+ )
937
+ [copied, pointers]
938
+ end
939
+
660
940
  # The binding reports the root element as parentless; the
661
941
  # moxml contract roots at the document.
662
942
  def root_parent(node)
@@ -664,14 +944,25 @@ module Moxml
664
944
  end
665
945
 
666
946
  def next_sibling(node)
947
+ return node.next_sibling if NATIVE_READ_LAYER &&
948
+ node.is_a?(::Leptris::XML::NativeNode)
949
+
667
950
  node.next_sibling if node.is_a?(::Leptris::XML::Node)
668
951
  end
669
952
 
670
953
  def previous_sibling(node)
954
+ # Bridged, not the native sibling walk: the native layer
955
+ # exposes next_sibling only, and its children batch
956
+ # truncates at 512 — the predecessor beyond that would be
957
+ # wrong.
958
+ node = to_binding(node)
671
959
  node.previous_sibling if node.is_a?(::Leptris::XML::Node)
672
960
  end
673
961
 
674
962
  def document(node)
963
+ return doc_for(node) if NATIVE_READ_LAYER &&
964
+ node.is_a?(::Leptris::XML::NativeNode)
965
+
675
966
  case node
676
967
  when ::Leptris::XML::Document then node
677
968
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
@@ -680,7 +971,18 @@ module Moxml
680
971
  end
681
972
 
682
973
  def root(document)
683
- document.root
974
+ r = document.root
975
+ return nil if r.nil?
976
+
977
+ if NATIVE_READ_LAYER
978
+ # Through the same address-keyed cache as the children
979
+ # path: NativeNode.from alone mints a fresh object per
980
+ # call on 1.9.163.x, splitting the wrappers (issue #219).
981
+ native = canonical_native(document, r)
982
+ record_native_doc(native, document)
983
+ return native
984
+ end
985
+ r
684
986
  end
685
987
 
686
988
  def line_number(node)
@@ -691,6 +993,7 @@ module Moxml
691
993
  end
692
994
 
693
995
  def attributes(element)
996
+ element = to_binding(element)
694
997
  element.attribute_nodes
695
998
  end
696
999
 
@@ -707,6 +1010,7 @@ module Moxml
707
1010
  end
708
1011
 
709
1012
  def set_attribute(element, name, value)
1013
+ element = to_binding(element) if NATIVE_READ_LAYER
710
1014
  element[name.to_s] = value.to_s
711
1015
  end
712
1016
 
@@ -726,14 +1030,17 @@ module Moxml
726
1030
  end
727
1031
 
728
1032
  def get_attribute(element, name)
1033
+ element = to_binding(element)
729
1034
  element.attribute_nodes.find { |attr| attr.name == name.to_s }
730
1035
  end
731
1036
 
732
1037
  def get_attribute_value(element, name)
1038
+ element = to_binding(element)
733
1039
  element[name.to_s]
734
1040
  end
735
1041
 
736
1042
  def remove_attribute(element, name)
1043
+ element = to_binding(element)
737
1044
  element.remove_attribute(name.to_s)
738
1045
  end
739
1046
 
@@ -743,6 +1050,10 @@ module Moxml
743
1050
  end
744
1051
 
745
1052
  def add_child(parent, child)
1053
+ if NATIVE_READ_LAYER
1054
+ parent = to_binding(parent)
1055
+ child = to_binding(child)
1056
+ end
746
1057
  case parent
747
1058
  when ::Leptris::XML::Document then add_document_child(parent, child)
748
1059
  else
@@ -759,6 +1070,7 @@ module Moxml
759
1070
  end
760
1071
 
761
1072
  def add_previous_sibling(node, new_node)
1073
+ node = to_binding(node) if NATIVE_READ_LAYER
762
1074
  # A PI inserted before the root lives at document level in
763
1075
  # libleptris's model, not in the element tree.
764
1076
  if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
@@ -770,10 +1082,12 @@ module Moxml
770
1082
  end
771
1083
 
772
1084
  def add_next_sibling(node, new_node)
1085
+ node = to_binding(node) if NATIVE_READ_LAYER
773
1086
  node.add_next_sibling(new_node)
774
1087
  end
775
1088
 
776
1089
  def remove(node)
1090
+ node = to_binding(node) if NATIVE_READ_LAYER
777
1091
  case node
778
1092
  when CustomizedLeptris::Declaration
779
1093
  remove_declaration(node.parent_doc) if node.parent_doc
@@ -793,6 +1107,10 @@ module Moxml
793
1107
  end
794
1108
 
795
1109
  def replace(node, new_node)
1110
+ if NATIVE_READ_LAYER
1111
+ node = to_binding(node)
1112
+ new_node = to_binding(new_node)
1113
+ end
796
1114
  return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
797
1115
 
798
1116
  # libleptris only offers element-anchored insertion, so a
@@ -817,10 +1135,20 @@ module Moxml
817
1135
  end
818
1136
 
819
1137
  def replace_children(node, new_children)
1138
+ if NATIVE_READ_LAYER
1139
+ node = to_binding(node)
1140
+ new_children = new_children.map { |child| to_binding(child) }
1141
+ end
820
1142
  node.children = new_children
821
1143
  end
822
1144
 
823
1145
  def text_content(node)
1146
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1147
+ return node.content if NATIVE_STRINGS_UTF8
1148
+
1149
+ return node.content.dup.force_encoding(Encoding::UTF_8)
1150
+ end
1151
+
824
1152
  # Frequency-ordered: elements dominate reads; they paid
825
1153
  # three failed compares to reach the else arm. The
826
1154
  # duplicated branch bodies are the point.
@@ -837,6 +1165,7 @@ module Moxml
837
1165
  end
838
1166
 
839
1167
  def inner_text(node)
1168
+ node = to_binding(node) if NATIVE_READ_LAYER
840
1169
  # moxml semantic: direct text children only — no descendant
841
1170
  # text (that is #text), no comments. Entity references
842
1171
  # contribute their serialized form.
@@ -851,6 +1180,7 @@ module Moxml
851
1180
  end
852
1181
 
853
1182
  def set_text_content(node, content)
1183
+ node = to_binding(node) if NATIVE_READ_LAYER
854
1184
  case node
855
1185
  when ::Leptris::XML::Document
856
1186
  node.root&.content = content.to_s
@@ -899,6 +1229,7 @@ module Moxml
899
1229
  end
900
1230
 
901
1231
  def namespace_definitions(element)
1232
+ element = to_binding(element) if NATIVE_READ_LAYER
902
1233
  element.namespace_definitions
903
1234
  end
904
1235
 
@@ -929,6 +1260,7 @@ module Moxml
929
1260
  NATIVE_GATE_CACHE = XPath::Cache.new(100)
930
1261
 
931
1262
  def xpath(node, expression, namespaces = {})
1263
+ node = to_binding(node) if NATIVE_READ_LAYER
932
1264
  native = native_xpath(node, expression, namespaces)
933
1265
  return native unless native.nil?
934
1266
 
@@ -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.41"
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.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-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