moxml 0.5.109 → 0.5.111

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: 04f63f1599959406386523aafe8289335b021ee5b729b0ac88dec032e08fac55
4
- data.tar.gz: f4d6cbd0012ef39253c519edd2af087c8ff230b7689622bad6143138e21c9d7d
3
+ metadata.gz: 24c16db3331672007afc4d08d042c3c44b52cfd00b3c20a026d88646afc842a7
4
+ data.tar.gz: 412edab11fe3895ef32a0f0520fb819fd2b5ea01b4436f08d715c0b38b132d1f
5
5
  SHA512:
6
- metadata.gz: f18cb5879bc64127726fc090c2c0f570edb813a032186e883d0c8ac3af7c96ec544f5c3fae7ca3d760b4fb5aba8ac5a1fb2a6a1422e27e7029f074543a036389
7
- data.tar.gz: a22ade19383fc5da7052f713761fe7bd458e062c59de10e6c8ab00d07f747a00b9627d7503148092630c798359074cbcabb05b91512b1953fd237087beafc2b5
6
+ metadata.gz: dc6537c232494b1b33ec4f4f821e392ca38e8f274e01d0973f5725449b6688c5ba5ec598b50ef74dbb3a9d0b26fce15f411bde9d93d02744b0d663482e603d05
7
+ data.tar.gz: '019eb6bacde42ea30d76d6207da5f95d5c05e907c6f144e7f4c21cf92790bc150780f9289ac88fdc5ac94223f862ba198096bd040c90d26f146f54e52c6dc797'
@@ -146,6 +146,21 @@ module Moxml
146
146
  NATIVE_SAX_RECORDS =
147
147
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.242")
148
148
 
149
+ # Document#absorb (engine #1548): one call transfers a source
150
+ # document's pool ownership into the destination, so
151
+ # cross-document attaches of its nodes move by reference (O(1),
152
+ # no deep copy) instead of rebuilding or copying at every seam.
153
+ # Floor 1.9.311.3, not .0: the .0 finalizer double-freed pools
154
+ # whose absorbed sources survived to process exit, and its
155
+ # splice paths dropped absorbed nodes (both fixed in .3 —
156
+ # leptris-ruby#386). The method_defined probe pairs with the
157
+ # version gate (misrelease races — the NATIVE_PLAN_STRUCTS
158
+ # lesson).
159
+ NATIVE_DOC_ABSORB =
160
+ defined?(::Leptris::XML::Document) &&
161
+ ::Leptris::XML::Document.method_defined?(:absorb) &&
162
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.311.3")
163
+
149
164
  # XSLT 1.0/2.0/3.0 through the engine's compiled-stylesheet
150
165
  # faces (Moxml::XSLT contract). Top-level parameters are not
151
166
  # threaded through the faces yet (leptris-ruby#360) — non-empty
@@ -1921,13 +1936,17 @@ module Moxml
1921
1936
  # collected its arena dies and the subtree vanishes (empty
1922
1937
  # output, nameless element pairs — silent, GC-dependent). Pin
1923
1938
  # the owning document on the target's document so the arena
1924
- # outlives every attached subtree.
1939
+ # outlives every attached subtree. Absorb (1.9.311) is NOT
1940
+ # taken here: the binding's splice paths are unreliable for
1941
+ # absorbed sources (transitively-stale handles drop nodes), so
1942
+ # only the element moves that route through the raw engine add
1943
+ # (add_child / sibling inserts below) absorb first.
1925
1944
  def pin_foreign_owner_document(parent, child)
1926
1945
  return unless child.is_a?(NN) || child.is_a?(::Leptris::XML::Node)
1927
1946
 
1928
1947
  # Two document fetches + identity compare on the same-document
1929
- # hot path (every builder add_child); the WeakMap bridge and
1930
- # the pin only pay when the child actually comes from another
1948
+ # hot path (every builder add_child); the bridge and the
1949
+ # pin only pay when the child actually comes from another
1931
1950
  # document.
1932
1951
  child_doc = child.is_a?(NN) ? NN_DOCUMENT.bind_call(child) : child.document
1933
1952
  return if child_doc.nil?
@@ -1947,7 +1966,103 @@ module Moxml
1947
1966
  attachments.set(parent_doc, :adopted_docs, docs)
1948
1967
  end
1949
1968
 
1969
+ # True when new_node's owning document (or the absorber its
1970
+ # pool moved into) was absorbed into node's document: the pool
1971
+ # is already the destination's, so splices must not take the
1972
+ # adopt/copy paths. The engine re-parents pool ownership but
1973
+ # NOT the node→document handles (they keep answering the
1974
+ # original parse document), so the absorbed-sources set is the
1975
+ # only authority. The binding's splice paths also drop
1976
+ # absorbed-source nodes upstream (their absorbed_into? check
1977
+ # is not transitive) — route absorbed splices through the raw
1978
+ # engine faces.
1979
+ def source_absorbed_into?(node, new_node)
1980
+ return false unless NATIVE_DOC_ABSORB
1981
+
1982
+ node_doc = node.is_a?(NN) ? NN_DOCUMENT.bind_call(node) : node.document
1983
+ new_doc = new_node.is_a?(NN) ? NN_DOCUMENT.bind_call(new_node) : new_node.document
1984
+ return false if node_doc.nil? || new_doc.nil? || node_doc.equal?(new_doc)
1985
+
1986
+ owner = new_doc.is_a?(NN) ? to_binding(new_doc) : new_doc
1987
+ dest = node_doc.is_a?(NN) ? to_binding(node_doc) : node_doc
1988
+ attachments.get(dest, :absorbed_sources)&.include?(owner) || false
1989
+ end
1990
+
1991
+ # 1.9.311 (engine #1548): move semantics for doomed splice
1992
+ # sources. Absorbing the child's document into the target
1993
+ # transfers pool ownership, so every later attach of its nodes
1994
+ # moves by reference instead of copying (the #376 structural
1995
+ # rebuild and its pointer-identity loss disappear; the engine
1996
+ # re-parents node→document so the same-doc gates resolve).
1997
+ # Absorb exactly once per (dest, owner): re-absorbing is an
1998
+ # engine error. On success the source's free becomes
1999
+ # handle-only — the reverse attachment pins the new pool owner
2000
+ # on the source, so a living source can never outlive the
2001
+ # memory its reads still touch.
2002
+ def absorb_source_document(dest, owner)
2003
+ return nil unless NATIVE_DOC_ABSORB
2004
+ return :direct if attachments.get(dest, :absorbed_sources)&.include?(owner)
2005
+
2006
+ # The child's document handle keeps answering its original
2007
+ # parse document even after that document's pool was
2008
+ # absorbed upward — the pool follows the absorber. On a
2009
+ # rejected absorb (already owned elsewhere), chase the
2010
+ # absorber chain and absorb the current pool owner. Returns
2011
+ # :direct on a first-attempt absorb, :chased when the pool
2012
+ # had to be taken from an upstream absorber (the binding's
2013
+ # splice paths drop such nodes — their absorbed_into? checks
2014
+ # are not transitive — so only raw engine faces may follow a
2015
+ # chase), nil when absorb is unavailable/refused.
2016
+ status = :direct
2017
+ candidate = owner
2018
+ while candidate
2019
+ begin
2020
+ dest.absorb(candidate)
2021
+ rescue ::Leptris::XML::Error, ::RuntimeError
2022
+ status = :chased
2023
+ candidate = attachments.get(candidate, :absorbed_by)
2024
+ next
2025
+ end
2026
+ sources = attachments.get(dest, :absorbed_sources) || []
2027
+ sources << candidate
2028
+ sub = attachments.get(candidate, :absorbed_sources)
2029
+ sources.concat(sub) if sub
2030
+ sources << owner unless sources.include?(owner)
2031
+ attachments.set(dest, :absorbed_sources, sources)
2032
+ # Un-pin: a pinned absorbed owner lives until exit and
2033
+ # its finalizer double-frees the transferred pool
2034
+ # (leptris-ruby#386).
2035
+ docs = attachments.get(dest, :adopted_docs)
2036
+ if docs
2037
+ docs.delete(owner)
2038
+ attachments.set(dest, :adopted_docs, docs)
2039
+ end
2040
+ attachments.set(candidate, :absorbed_by, dest)
2041
+ attachments.set(owner, :absorbed_by, dest) unless owner.equal?(candidate)
2042
+ return status
2043
+ end
2044
+ nil
2045
+ end
2046
+
1950
2047
  def add_child(parent, child)
2048
+ # 1.9.311: binding-family children (created PIs, texts...) —
2049
+ # a DIRECT absorb makes the binding add below move the node
2050
+ # by reference. A source already owned elsewhere would need
2051
+ # a chase, and the binding's splice drops chased sources
2052
+ # upstream (its absorbed_into? checks are not transitive) —
2053
+ # those keep the pin + adoption copy. Entity references
2054
+ # keep the marker flow below.
2055
+ if NATIVE_DOC_ABSORB && !child.is_a?(NN) &&
2056
+ child.is_a?(::Leptris::XML::Node) &&
2057
+ !child.is_a?(CustomizedLeptris::EntityReference)
2058
+ child_doc = child.document
2059
+ parent_doc = parent.is_a?(NN) ? NN_DOCUMENT.bind_call(parent) : parent.document
2060
+ if child_doc && parent_doc && !child_doc.equal?(parent_doc)
2061
+ owner = to_binding(child_doc)
2062
+ absorb_source_document(to_binding(parent_doc), owner) if
2063
+ attachments.get(owner, :absorbed_by).nil?
2064
+ end
2065
+ end
1951
2066
  if NATIVE_READ_LAYER && !(NATIVE_MUTATIONS_COHERENT &&
1952
2067
  parent.is_a?(::Leptris::XML::NativeNode) &&
1953
2068
  child.is_a?(::Leptris::XML::NativeNode))
@@ -1973,12 +2088,19 @@ module Moxml
1973
2088
  # binding's create faces — correct tree; the caller's
1974
2089
  # wrapper keeps addressing the source original (pointer
1975
2090
  # identity waits on the upstream installed-handle face).
1976
- # Non-element kinds keep the plain C add.
2091
+ # 1.9.311 (engine #1548): absorb the source document
2092
+ # first and the plain add MOVES the element by reference
2093
+ # (O(1), no copy, no live-set doubling) — the rebuild
2094
+ # only fires when absorb is refused. Non-element kinds
2095
+ # keep the pin + plain C add: the binding's splices drop
2096
+ # absorbed-source nodes upstream.
1977
2097
  child_doc = NN_DOCUMENT.bind_call(child)
1978
2098
  parent_doc = NN_DOCUMENT.bind_call(parent)
1979
2099
  if child_doc && parent_doc &&
1980
2100
  !child_doc.equal?(parent_doc) && child.node_type == :element
1981
- return structural_adopt(parent, child)
2101
+ absorb_source_document(to_binding(parent_doc), to_binding(child_doc))
2102
+ return structural_adopt(parent, child) unless
2103
+ source_absorbed_into?(parent, child)
1982
2104
  end
1983
2105
 
1984
2106
  doc = NN_DOCUMENT.bind_call(parent)
@@ -2015,7 +2137,18 @@ module Moxml
2015
2137
  node.document.add_pi(new_node.target, new_node.content.to_s)
2016
2138
  return new_node
2017
2139
  end
2018
- return node.add_previous_sibling(new_node) if node.is_a?(::Leptris::XML::Element)
2140
+ # 1.9.311: an absorbed element source moves through the raw
2141
+ # engine faces — the binding's sibling insert drops it.
2142
+ if new_node.is_a?(::Leptris::XML::Element) &&
2143
+ !new_node.is_a?(::Leptris::XML::NativeNode)
2144
+ new_doc = new_node.document
2145
+ node_doc = node.document
2146
+ if new_doc && node_doc && !new_doc.equal?(node_doc)
2147
+ absorb_source_document(node_doc, new_doc)
2148
+ end
2149
+ end
2150
+ return node.add_previous_sibling(new_node) if node.is_a?(::Leptris::XML::Element) &&
2151
+ !source_absorbed_into?(node, new_node)
2019
2152
 
2020
2153
  # Non-element receivers: raw-FFI anchor (issue #245, same
2021
2154
  # shape as add_next_sibling).
@@ -2034,7 +2167,18 @@ module Moxml
2034
2167
  new_node = to_binding(new_node)
2035
2168
  end
2036
2169
  pin_foreign_owner_document(node, new_node)
2037
- return node.add_next_sibling(new_node) if node.is_a?(::Leptris::XML::Element)
2170
+ # 1.9.311: an absorbed element source moves through the raw
2171
+ # engine faces — the binding's sibling insert drops it.
2172
+ if new_node.is_a?(::Leptris::XML::Element) &&
2173
+ !new_node.is_a?(::Leptris::XML::NativeNode)
2174
+ new_doc = new_node.document
2175
+ node_doc = node.document
2176
+ if new_doc && node_doc && !new_doc.equal?(node_doc)
2177
+ absorb_source_document(node_doc, new_doc)
2178
+ end
2179
+ end
2180
+ return node.add_next_sibling(new_node) if node.is_a?(::Leptris::XML::Element) &&
2181
+ !source_absorbed_into?(node, new_node)
2038
2182
 
2039
2183
  # Non-element receivers (Text/Comment/CDATA): the binding's
2040
2184
  # method is Element-only, but the C insert anchors on any
@@ -2132,6 +2276,10 @@ module Moxml
2132
2276
  # content node (text/comment/CDATA/PI) is replaced by
2133
2277
  # unlinking and re-inserting: after an element sibling when
2134
2278
  # one exists, else appended (end-of-list) to the parent.
2279
+ # Raw engine faces, not the binding add: a parent resolved
2280
+ # through an absorbed source's stale document handle sends
2281
+ # same-pool adds down the adoption path, which drops
2282
+ # non-element kinds (leptris-ruby#376 family).
2135
2283
  parent = node.parent
2136
2284
  unless parent
2137
2285
  raise Moxml::DocumentStructureError.new(
@@ -2142,9 +2290,17 @@ module Moxml
2142
2290
  prev = node.previous_sibling
2143
2291
  node.unlink
2144
2292
  if prev.is_a?(::Leptris::XML::Element)
2145
- prev.add_next_sibling(new_node)
2293
+ ::Leptris::XML::FFI.check_status(
2294
+ ::Leptris::XML::FFI.leptris_element_insert_after(
2295
+ prev.c_ptr, new_node.c_ptr
2296
+ ),
2297
+ )
2146
2298
  else
2147
- parent.add_child(new_node)
2299
+ ::Leptris::XML::FFI.check_status(
2300
+ ::Leptris::XML::FFI.leptris_element_append_child(
2301
+ parent.c_ptr, new_node.c_ptr
2302
+ ),
2303
+ )
2148
2304
  end
2149
2305
  new_node
2150
2306
  end
data/lib/moxml/element.rb CHANGED
@@ -127,8 +127,8 @@ module Moxml
127
127
  # writes bump it (see invalidate_attribute_value_cache!).
128
128
  def attribute_pairs
129
129
  generation = attribute_value_generation
130
- @pairs = nil if @pairs_gen != generation
131
- @pairs_gen = generation
130
+ @attribute_pairs = nil if @attribute_pairs_gen != generation
131
+ @attribute_pairs_gen = generation
132
132
  @attribute_pairs ||= adapter.attribute_pairs(@native)
133
133
  end
134
134
 
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.109"
4
+ VERSION = "0.5.111"
5
5
  end
@@ -64,22 +64,12 @@ 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
74
67
  element.add_child(pi)
75
68
  pi.remove
76
69
  expect(element.children).to be_empty
77
70
  end
78
71
 
79
72
  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
83
73
  element.add_child(pi)
84
74
  text = doc.create_text("replacement")
85
75
  pi.replace(text)
@@ -2,13 +2,12 @@
2
2
 
3
3
  require "spec_helper"
4
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.
5
+ # Cross-document adoption contract. leptris >= 1.9.311 absorbs the
6
+ # source document's pool into the destination (engine #1548) so
7
+ # attaches move by reference — wrapper identity survives; older
8
+ # bindings deep-copy at the splice (leptris-ruby#1528) and only the
9
+ # ADOPTION shape is pinned (the copy lands in the tree, correct
10
+ # names, doc-owned).
12
11
  RSpec.describe "leptris cross-document adoption" do
13
12
  let(:ctx) { Moxml.new(:leptris) }
14
13
 
@@ -31,4 +30,74 @@ RSpec.describe "leptris cross-document adoption" do
31
30
  expect(doc.root.children.to_a[1].name).to eq("x")
32
31
  expect(doc.at("//r/x")).not_to be_nil
33
32
  end
33
+
34
+ context "when the binding offers Document#absorb (>= 1.9.311)" do
35
+ before do
36
+ unless Moxml::Adapter::Leptris::NATIVE_DOC_ABSORB
37
+ skip "binding #{Leptris::VERSION} has no absorb face"
38
+ end
39
+ end
40
+
41
+ it "moves the child by reference — the wrapper addresses the attached node" do
42
+ doc = ctx.parse("<r><a/></r>")
43
+ other = ctx.parse("<other><new/></other>")
44
+ child = other.root.children.first
45
+ doc.root.add_child(child)
46
+ expect(doc.root.children.last).to eq(child)
47
+ expect(doc.root.to_xml).not_to include("<other")
48
+ child["probe"] = "1"
49
+ expect(doc.at("//r/new")["probe"]).to eq("1")
50
+ expect(child.parent.name).to eq("r")
51
+ end
52
+
53
+ it "keeps the pre-311 adoption behavior for non-element kinds" do
54
+ # Absorb is scoped to element moves: the binding's splice paths
55
+ # drop absorbed-source nodes upstream (their absorbed_into?
56
+ # check is not transitive), so binding-family children keep
57
+ # the 1.9.304 adoption copy.
58
+ doc = ctx.parse("<r/>")
59
+ other = ctx.parse("<other/>")
60
+ pi = other.create_processing_instruction("xml-stylesheet", 'href="s.xsl"')
61
+ other.root.add_child(pi)
62
+ doc.root.add_child(pi)
63
+ expect(doc.root.to_xml).to include("<?xml-stylesheet")
64
+ end
65
+
66
+ it "keeps the tree alive after the source document is collected" do
67
+ doc = ctx.parse("<r/>")
68
+ child = nil
69
+ 3.times do
70
+ other = ctx.parse("<other><deep><leaf v='1'>t</leaf></deep></other>")
71
+ child = other.root.children.first
72
+ doc.root.add_child(child)
73
+ nil
74
+ GC.start
75
+ end
76
+ expect(doc.at("//r/deep/leaf")["v"]).to eq("1")
77
+ expect(doc.at("//r/deep/leaf").text).to eq("t")
78
+ expect(child.name).to eq("deep")
79
+ end
80
+
81
+ it "keeps a living source readable after its pool is absorbed" do
82
+ doc = ctx.parse("<r/>")
83
+ other = ctx.parse("<other><x>1</x></other>")
84
+ doc.root.add_child(other.root.children.first)
85
+ expect(other.root.name).to eq("other")
86
+ expect(other.at("//other")).not_to be_nil
87
+ end
88
+
89
+ it "moves elements whose pool was absorbed through an intermediate document" do
90
+ # scratch → mid (first splice) → doc: the chase takes the pool
91
+ # from mid; the raw engine add then moves by pool identity.
92
+ doc = ctx.parse("<r/>")
93
+ mid = ctx.parse("<mid/>")
94
+ scratch = ctx.parse("<scratch><deep v='1'>t</deep></scratch>")
95
+ mid.root.add_child(scratch.root.children.first)
96
+ expect(mid.root.to_xml).to include("<deep")
97
+ doc.root.add_child(mid.root.children.first)
98
+ expect(doc.at("//r/deep")["v"]).to eq("1")
99
+ expect(doc.root.to_xml).not_to include("<mid>")
100
+ expect(mid.root.to_xml).not_to include("<deep")
101
+ end
102
+ end
34
103
  end
@@ -39,4 +39,23 @@ RSpec.describe Moxml::Element do
39
39
  end
40
40
  end
41
41
  end
42
+
43
+ context "when memoized" do
44
+ it "returns the same object on repeated reads" do
45
+ with_adapter(:leptris) do |ctx|
46
+ root = ctx.parse(xml).root
47
+ expect(root.attribute_pairs).to equal(root.attribute_pairs)
48
+ end
49
+ end
50
+
51
+ it "refreshes after a value write" do
52
+ with_adapter(:leptris) do |ctx|
53
+ root = ctx.parse(xml).root
54
+ before = root.attribute_pairs
55
+ root["id"] = "r9"
56
+ expect(root.attribute_pairs).not_to equal(before)
57
+ expect(root.attribute_pairs).to include(["id", "r9"])
58
+ end
59
+ end
60
+ end
42
61
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.109
4
+ version: 0.5.111
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.