moxml 0.5.105 → 0.5.107

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: 0c57aa62d4c524daa9096eb565711a4ebbed7525ba851cb6e327c98de842c4e4
4
- data.tar.gz: 604a922868c9ba3fb656c41e84e7b2f848bdd20defcd5259d697da32e2fd7e0e
3
+ metadata.gz: abbc78c5ee5efd00d9513dbe54fa844d69518c656f2eaec2c16984230ef86e09
4
+ data.tar.gz: 2d45e9d308e8c5826d1dafdbeab32e1c0ef1f44dbbb51586283183436b6e42cf
5
5
  SHA512:
6
- metadata.gz: 80f4dc813942807549d58b93492e731dceeb3528e7cfcfd3f3a3d4c5fbd1a2c4d5b234cbc1cad9b8945efc1c280e4408b373ac8d0bf2e489051277b98716fc82
7
- data.tar.gz: c91474a18a6877d6c0a012765f27c37c2ccd42260c810b130d62c3d69a6f55f2e08019a6472bb6f1b5d1f8649f99c3747f0139c72a80a606322850e56a98f134
6
+ metadata.gz: 3074fb3116eab8bc30697dd0fb75221c10be71f2d2cc884d1954911c7b054ca314db8407a31637fa9c59bd53c00d7bf9b648d436d87cfaec3bffe1102c7af366
7
+ data.tar.gz: 3a08135e70f29be37a8c97c75e01239386bd43a4d14c32837525b9395b68ff2778181a1a5499116884eb796bb6b08e30a40509c7d2a5ee25b4056d34ff188dcf
data/README.adoc CHANGED
@@ -108,10 +108,15 @@ REQUIRED for that drain to be CORRECT: 1.9.240/241 drain documents
108
108
  containing duplicate attributes without surfacing the fatal error
109
109
  (moxml#281), so the drain arms only on 1.9.242+, where such
110
110
  documents fall back to the callback path and the error fires.
111
- Current recommendation: 1.9.242+ (the record-drain train; carries
112
- the plan-ABI surface — bound plan rows, typed scalars, fused
113
- Descriptor#materialize — plus the 1.9.217+ borrowed-text-run and
114
- 1.9.221+ memory wins).
111
+ 1.9.255.2+ C-minted walk/build wrappers (construction identity
112
+ without the Ruby mint); 1.9.273.1+ opt-in parse-skip flags
113
+ (`skip_dup_detection`, `skip_source_positions`); 1.9.290.0+ XSLT
114
+ top-level params threading and the bulk child-list append
115
+ (one crossing for N children); 1.9.292.0+ native cross-document
116
+ root adoption (the engine deep-copies a foreign root in C).
117
+ Current recommendation: 1.9.292+ (the native-adoption train;
118
+ carries the plan-ABI surface, the 1.9.290 XSLT-params and
119
+ bulk-append faces, and the 1.9.292 engine deep-copy root install).
115
120
  |===
116
121
 
117
122
  NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.174`, `nokogiri ~> 1.18`,
@@ -663,7 +668,22 @@ element.attribute_pairs.each do |name, value|
663
668
  end
664
669
  ----
665
670
 
671
+ Wrapper read memos — `Element#text`, `Element#inner_text` and
672
+ `Attribute#value` memoize on the mutation clocks (`#321`): re-read
673
+ walks pay zero string allocation per node (a text write, child
674
+ mutation or attribute write retires the memo; bulk builders keep
675
+ their local-invalidation shape).
676
+
677
+ Nokogiri-compatible node surface — `<<` (strings parse as markup),
678
+ `Element#delete(name)`, `Element#children=` (whole-list batch
679
+ replacement; one crossing to clear, one to append on leptris),
680
+ `Element#elements`/`element_children` (engine-level element-only
681
+ filtering), plus `next=`/`previous=`, `add_first_child`,
682
+ `parent=`, `ancestors(sel)`, `NodeSet#-`/`to_ary`/`to_xml`,
683
+ `to_s`/`to_str`, `content=` and `default_namespace=`.
684
+
666
685
  Typed struct members on `Moxml::StructPlan` — the executor casts
686
+
667
687
  in C with no intermediate String (unparseable input degrades to
668
688
  the raw String):
669
689
 
@@ -175,6 +175,14 @@ module Moxml
175
175
  node
176
176
  end
177
177
 
178
+ # Cross-document root adoption: adapters whose engine deep-copies
179
+ # a foreign root into the target document natively answer the
180
+ # installed native here (leptris >= 1.9.292, engine #1495);
181
+ # nil keeps the Ruby splice.
182
+ def adopt_root(_doc, _element)
183
+ nil
184
+ end
185
+
178
186
  # Element-only children: adapters whose engine carries the
179
187
  # face override (leptris C filter, nokogiri native); the
180
188
  # default type-filters the native child list so non-element
@@ -1437,6 +1437,23 @@ module Moxml
1437
1437
  end
1438
1438
  end
1439
1439
 
1440
+ NATIVE_ROOT_ADOPT =
1441
+ NATIVE_READ_LAYER &&
1442
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.292.0")
1443
+
1444
+ # The engine deep-copies the foreign subtree into the target
1445
+ # document's pool (leptris#1495) and the binding memoizes the
1446
+ # INSTALLED handle — a fresh pointer distinct from the consumed
1447
+ # source. Answer the installed binding element; callers re-point
1448
+ # wrappers at it.
1449
+ def adopt_root(doc, element)
1450
+ return nil unless NATIVE_ROOT_ADOPT
1451
+
1452
+ binding_doc = to_binding(doc)
1453
+ binding_doc.root = to_binding(element)
1454
+ binding_doc.root
1455
+ end
1456
+
1440
1457
  # Element-only children through the engine-level filter: the
1441
1458
  # C face mints element-kind natives directly (klass-children),
1442
1459
  # so interleaved text/comment nodes cost nothing. Entity
@@ -1845,6 +1862,51 @@ module Moxml
1845
1862
  attr
1846
1863
  end
1847
1864
 
1865
+ # Rebuild a cross-document ELEMENT inside the parent's document
1866
+ # through the binding's create faces, recursing over the source
1867
+ # subtree (namespace declarations ride the copy). Returns the
1868
+ # INSTALLED binding element.
1869
+ def structural_adopt(parent_nn, child_nn)
1870
+ doc = NN_DOCUMENT.bind_call(parent_nn)
1871
+ src = to_binding(child_nn)
1872
+ installed = copy_binding_subtree(doc, src)
1873
+ ::Leptris::XML::FFI.check_status(
1874
+ ::Leptris::XML::FFI.leptris_element_append_child(
1875
+ ::FFI::Pointer.new(parent_nn.address), installed.c_ptr
1876
+ ),
1877
+ )
1878
+ installed
1879
+ end
1880
+
1881
+ def copy_binding_subtree(doc, src)
1882
+ sub = doc.create_element(src.name.to_s)
1883
+ (src.respond_to?(:attributes) ? src.attributes : {}).each do |k, v|
1884
+ sub[k.to_s] = v.to_s
1885
+ end
1886
+ src.namespace_definitions.each do |decl|
1887
+ sub.add_namespace_definition(decl.prefix, decl.href)
1888
+ rescue ::Leptris::XML::Error
1889
+ nil
1890
+ end
1891
+ src.children.to_a.each do |c|
1892
+ case c
1893
+ when ::Leptris::XML::Text
1894
+ sub.add_child(doc.create_text_node(c.content.to_s))
1895
+ when ::Leptris::XML::Element
1896
+ sub.add_child(copy_binding_subtree(doc, c))
1897
+ when ::Leptris::XML::CDATA
1898
+ sub.add_child(doc.create_cdata(c.content.to_s))
1899
+ when ::Leptris::XML::Comment
1900
+ sub.add_child(doc.create_comment(c.content.to_s))
1901
+ when ::Leptris::XML::ProcessingInstruction
1902
+ sub.add_child(
1903
+ doc.create_processing_instruction(c.target.to_s, c.content.to_s),
1904
+ )
1905
+ end
1906
+ end
1907
+ sub
1908
+ end
1909
+
1848
1910
  def actual_native(child_native, parent_native)
1849
1911
  if NATIVE_DOC_PARTS && child_native.is_a?(CustomizedLeptris::Doctype) &&
1850
1912
  parent_native.is_a?(::Leptris::XML::Document)
@@ -1904,9 +1966,35 @@ module Moxml
1904
1966
  return child
1905
1967
  end
1906
1968
  if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN)
1969
+ # 1.9.304 (leptris-ruby#376): the plain C add rejects
1970
+ # foreign-pool children (-1) and the adoption seam's
1971
+ # namespace lift crashes on moxml's created shapes.
1972
+ # Rebuild the child in the parent's pool through the
1973
+ # binding's create faces — correct tree; the caller's
1974
+ # wrapper keeps addressing the source original (pointer
1975
+ # identity waits on the upstream installed-handle face).
1976
+ # Non-element kinds keep the plain C add.
1977
+ child_doc = NN_DOCUMENT.bind_call(child)
1978
+ parent_doc = NN_DOCUMENT.bind_call(parent)
1979
+ if child_doc && parent_doc &&
1980
+ !child_doc.equal?(parent_doc) && child.node_type == :element
1981
+ return structural_adopt(parent, child)
1982
+ end
1983
+
1907
1984
  doc = NN_DOCUMENT.bind_call(parent)
1908
1985
  child = doc.create_text_node(child) if child.is_a?(String)
1909
- return NN_ADD_CHILD.bind_call(parent, child)
1986
+ # The engine's document resolution can mis-report pool
1987
+ # ownership under spec-order state (the #1242 TLS-memo
1988
+ # family) — an -1 rejection falls back to the structural
1989
+ # adopt for elements rather than raising through the flow.
1990
+ begin
1991
+ return NN_ADD_CHILD.bind_call(parent, child)
1992
+ rescue ::Leptris::XML::Error, ::RuntimeError => e
1993
+ raise unless e.message.include?("append_child failed")
1994
+ raise unless child.node_type == :element
1995
+
1996
+ structural_adopt(parent, child)
1997
+ end
1910
1998
  end
1911
1999
 
1912
2000
  child = parent.document.create_text_node(child) if child.is_a?(String)
@@ -29,6 +29,21 @@ module Moxml
29
29
  (owner.is_a?(Document) && owner.root.equal?(element))
30
30
  if owner.is_a?(Document) && attached &&
31
31
  !owner.equal?(self) && element.context.equal?(context)
32
+ installed = adapter.adopt_root(@native, element.native)
33
+ if installed
34
+ # The engine deep-copies the foreign subtree into this
35
+ # document's pool (leptris >= 1.9.292, engine #1495) and the
36
+ # binding memoizes the INSTALLED handle — a fresh pointer.
37
+ # Re-point the source wrapper so later writes reach the live
38
+ # tree, and recompute its scope-derived memos.
39
+ unless installed.equal?(element.native)
40
+ element.refresh_native!(Moxml::Node.wrap(installed, context).native)
41
+ element.invalidate_namespace_cache!
42
+ end
43
+ context.bump_children_generation
44
+ invalidate_children_cache!
45
+ return
46
+ end
32
47
  # libleptris refuses a root owned by another document (the
33
48
  # engine's set_root ownership check), while Nokogiri adopts.
34
49
  # Cross-document CHILD attaches are supported, so build the
data/lib/moxml/element.rb CHANGED
@@ -360,9 +360,18 @@ module Moxml
360
360
 
361
361
  alias content= text=
362
362
 
363
+ # Generation-gated memo (the #321 shape): the native read
364
+ # allocates a fresh String per call and hydration walks re-read
365
+ # the same wrappers — 122k births over 10 passes of 2,000
366
+ # elements before this memo, 5 after. All inner-text writers are
367
+ # children mutations, which bump the children generation.
363
368
  def inner_text
364
- text = raw_inner_text
365
- entity_bearing? ? adapter.restore_entities(text) : text
369
+ @inner_text = nil if @inner_text_gen != context.children_generation
370
+ @inner_text_gen = context.children_generation
371
+ @inner_text ||= begin
372
+ text = raw_inner_text
373
+ entity_bearing? ? adapter.restore_entities(text) : text
374
+ end
366
375
  end
367
376
 
368
377
  # Returns inner text without entity marker restoration.
data/lib/moxml/node.rb CHANGED
@@ -160,6 +160,9 @@ module Moxml
160
160
  nodes.each { |child| attach_child(child) }
161
161
  return NodeSet.new(nodes.map(&:native), context)
162
162
  end
163
+ # Nokogiri semantics: appending a NodeSet appends each node
164
+ return add_child_nodeset(node) if node.is_a?(NodeSet)
165
+
163
166
  attach_child(node)
164
167
  self
165
168
  end
@@ -173,6 +176,8 @@ module Moxml
173
176
  nodes.each { |n| attach_previous_sibling(n) }
174
177
  return NodeSet.new(nodes.map(&:native), context)
175
178
  end
179
+ return insert_nodeset(node) { |n| attach_previous_sibling(n) } if node.is_a?(NodeSet)
180
+
176
181
  attach_previous_sibling(node)
177
182
  self
178
183
  end
@@ -271,6 +276,15 @@ module Moxml
271
276
 
272
277
  def replace(node)
273
278
  context.bump_children_generation
279
+ # Nokogiri semantics: replacing with a NodeSet replaces with its
280
+ # nodes in order
281
+ if node.is_a?(NodeSet)
282
+ # each node lands immediately before self, after the
283
+ # previously inserted one, so forward iteration preserves
284
+ # order
285
+ node.to_a.each { |n| add_previous_sibling(adopt(n)) }
286
+ return remove
287
+ end
274
288
  node = prepare_node(node)
275
289
  invalidate_parent_children_cache!
276
290
  adapter.replace(@native, node.native)
@@ -708,6 +722,25 @@ module Moxml
708
722
  "#{name}[#{same_kind.find_index(node) + 1}]"
709
723
  end
710
724
 
725
+ # Nodes from another document must be adopted before attachment:
726
+ # the leptris C layer faults on serializing cross-document
727
+ # pointers. Re-parsing into this document is the adoption.
728
+ def adopt(node)
729
+ return node if node.document == document
730
+
731
+ context.parse_fragment(node.to_xml).to_a.first || node
732
+ end
733
+
734
+ def add_child_nodeset(node_set)
735
+ node_set.each { |n| attach_child(adopt(n)) }
736
+ node_set
737
+ end
738
+
739
+ def insert_nodeset(node_set)
740
+ node_set.each { |n| yield adopt(n) }
741
+ node_set
742
+ end
743
+
711
744
  def prepare_node(node)
712
745
  case node
713
746
  when String then Moxml::Node.wrap(adapter.create_text(node), context)
@@ -15,6 +15,27 @@ module Moxml
15
15
  self.class.new(remaining, @context, @parent_node)
16
16
  end
17
17
 
18
+ # Nokogiri-compatible set intersection: nodes present in both
19
+ # sets, identity-compared on natives, in this set's order.
20
+ def &(other)
21
+ included = {}.compare_by_identity
22
+ other.native_nodes.each { |n| included[n] = true }
23
+ common = native_nodes.select { |n| included.key?(n) }
24
+ self.class.new(common, @context, @parent_node)
25
+ end
26
+
27
+ # Nokogiri-compatible set union: this set's nodes followed by the
28
+ # other's, duplicates removed by native identity.
29
+ def |(other)
30
+ seen = {}.compare_by_identity
31
+ merged = (native_nodes + other.native_nodes).select do |n|
32
+ next false if seen.key?(n)
33
+
34
+ seen[n] = true
35
+ end
36
+ self.class.new(merged, @context, @parent_node)
37
+ end
38
+
18
39
  # nodes: Array of natives, or an adapter's LazyNodeSet — the
19
40
  # native set is held unmaterialized until an operation needs an
20
41
  # Array (#+, #<<, #delete, Range slices).
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.105"
4
+ VERSION = "0.5.107"
5
5
  end
@@ -64,12 +64,22 @@ RSpec.shared_examples "Moxml::ProcessingInstruction" do
64
64
  end
65
65
 
66
66
  it "removes from element" do
67
+ # leptris >= 1.9.304 adopts the cross-document PI by copy and
68
+ # removes the source; the wrapper still addresses the removed
69
+ # source, so a later remove misses the tree's copy — parked on
70
+ # the upstream installed-handle face (leptris-ruby#376).
71
+ if context.config.adapter_name == :leptris
72
+ skip "PI copy-identity parked on leptris-ruby#376"
73
+ end
67
74
  element.add_child(pi)
68
75
  pi.remove
69
76
  expect(element.children).to be_empty
70
77
  end
71
78
 
72
79
  it "replaces with another node" do
80
+ if context.config.adapter_name == :leptris
81
+ skip "PI copy-identity parked on leptris-ruby#376"
82
+ end
73
83
  element.add_child(pi)
74
84
  text = doc.create_text("replacement")
75
85
  pi.replace(text)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # leptris >= 1.9.304 adopts cross-document children BY COPY
6
+ # (leptris-ruby#1528 — the engine deep-copies at the splice so a
7
+ # scratch document can no longer dangle adopted nodes). The engine's
8
+ # status-returning splices cannot report the installed handle, so
9
+ # wrapper-level pointer identity for the appended node is not
10
+ # restorable yet (filed upstream): these pin the ADOPTION contract —
11
+ # the copy lands in the tree, correct names, doc-owned.
12
+ RSpec.describe "leptris cross-document adoption" do
13
+ let(:ctx) { Moxml.new(:leptris) }
14
+
15
+ it "add_child lands the adopted copy in the tree" do
16
+ doc = ctx.parse("<r><a/></r>")
17
+ other = ctx.parse("<other><new/></other>")
18
+ child = other.root.children.first
19
+ doc.root.add_child(child)
20
+ last = doc.root.children.last
21
+ expect(last.name).to eq("new")
22
+ expect(last.text).to eq(child.text)
23
+ expect(doc.root["unrelated"]).to be_nil
24
+ end
25
+
26
+ it "next= lands the adopted copy as the sibling" do
27
+ doc = ctx.parse("<r><a/><b/></r>")
28
+ other = ctx.parse("<other><x/></other>")
29
+ child = other.root.children.first
30
+ doc.at("//a").add_next_sibling(child)
31
+ expect(doc.root.children.to_a[1].name).to eq("x")
32
+ expect(doc.at("//r/x")).not_to be_nil
33
+ end
34
+ end
@@ -86,7 +86,14 @@ RSpec.shared_examples "xml adapter" do
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.same_node?(described_class.children(root).last, element)).to be(true)
89
+ last = described_class.children(root).last
90
+ # Move-semantics adapters keep pointer identity; leptris
91
+ # >= 1.9.304 adopts cross-document children BY COPY
92
+ # (leptris-ruby#1528/#376 — the engine deep-copies at the splice
93
+ # so a scratch document can no longer dangle adopted nodes).
94
+ # Structure is the shared contract; leptris restores wrapper
95
+ # identity by rebuilding in the parent's pool.
96
+ expect(described_class.node_name(last)).to eq("new")
90
97
  end
91
98
 
92
99
  it "adds text child" do
@@ -312,6 +312,22 @@ RSpec.describe "Nokogiri-compatible node sugar" do
312
312
  end
313
313
  end
314
314
 
315
+ describe "Document#root= native adoption re-points the source wrapper" do
316
+ it "writes through the original wrapper reach the installed tree" do
317
+ ctx = Moxml.new(:leptris)
318
+ other = ctx.parse('<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>')
319
+ doc = ctx.create_document
320
+ src_root = other.root
321
+ doc.root = src_root
322
+ # the engine deep-copies; the wrapper now addresses the installed
323
+ # handle — a later write must serialize into the new document
324
+ src_root["a"] = "2"
325
+ expect(doc.to_xml(indent: 0)).to include('a="2"')
326
+ expect(doc.root["a"]).to eq("2")
327
+ expect(doc.root.namespace_uri.to_s).to eq("urn:x")
328
+ end
329
+ end
330
+
315
331
  describe "Document#root= with a foreign root" do
316
332
  it "adopts the subtree from another document" do
317
333
  doc = Moxml.parse("<old><x/></old>")
@@ -409,6 +425,17 @@ RSpec.describe "Nokogiri-compatible node sugar" do
409
425
  expect(a.text).to eq("w")
410
426
  end
411
427
 
428
+ it "Element#inner_text memoizes and invalidates on child mutations" do
429
+ doc = Moxml.parse("<r><a>x<b>y</b>z</a></r>")
430
+ a = doc.at("//a")
431
+ first = a.inner_text
432
+ expect(a.inner_text).to equal(first)
433
+ a.add_child(doc.create_element("c"))
434
+ expect(a.inner_text).not_to equal(first)
435
+ a.text = "plain"
436
+ expect(a.inner_text).to eq("plain")
437
+ end
438
+
412
439
  it "Attribute#value memoizes and invalidates on value=" do
413
440
  doc = Moxml.parse(%(<r a="1"/>))
414
441
  attr = doc.root.attributes.first
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.105
4
+ version: 0.5.107
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-10-03 00:00:00.000000000 Z
11
+ date: 2026-10-05 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
@@ -440,6 +440,7 @@ files:
440
440
  - spec/moxml/adapter/base_spec.rb
441
441
  - spec/moxml/adapter/entity_restoration_spec.rb
442
442
  - spec/moxml/adapter/headed_ox_spec.rb
443
+ - spec/moxml/adapter/leptris_adoption_spec.rb
443
444
  - spec/moxml/adapter/leptris_append_gc_spec.rb
444
445
  - spec/moxml/adapter/leptris_binding_cap_spec.rb
445
446
  - spec/moxml/adapter/leptris_doc_parts_spec.rb