moxml 0.5.112 → 0.5.114

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: 63a1ceed1b65075fbb6187383de91bba6fa8a4dc98d7b6c5e1a8b72c6cdbd5c8
4
- data.tar.gz: f60b17278dd6ce812b27e7bcae595fe464d3192d24bfd68c0b6fb98c285b4dea
3
+ metadata.gz: 6cc0d0436680fb9c46169bc93e45197b20b71bf6970d737750e63fb110a5e75e
4
+ data.tar.gz: 8f71ca799b845b4fb28a977f6515ac1b702b6557259667c5b034f5b612ef6b67
5
5
  SHA512:
6
- metadata.gz: 7a4fd16c91ce73fe9fd1ce72d617fe1d5009d454e5bf8418b1e96f36c804459e4b0186d593b813a84d9092fb9c5818f6b25a95aac6b69feedc34eeba2822ad26
7
- data.tar.gz: 7f1238c394d8ad91e18176ca7d863477ed968fc91bf093a5113885c33665a15a1f83a2696fa68131e039344905679739f97d4c0b1f5ef06b1baf438231ad74c3
6
+ metadata.gz: 1edf604a33151f0079608d52231a1486a252cda307f49fd958e05f41e3229394a952245159939c2f4dac67ecba9f612ef6c4718d7f450c3260e1f78fc4d83135
7
+ data.tar.gz: 92aeae211e0b8abae0ce569c9dbd650f3b2b8426e975a42e3341317ebeb6c09928adbb0265b2ea7cdb726ca40d9ac376be1e537200d2166255fc30f365afd7ed
data/.rubocop.yml CHANGED
@@ -23,6 +23,10 @@ AllCops:
23
23
  # the zero-allocation streaming form (issue #143). One parameter per
24
24
  # record key is the interface; a shorter list would need a wrapper
25
25
  # object, which is the allocation the API exists to avoid.
26
+ Metrics/BlockLength:
27
+ Exclude:
28
+ - 'Rakefile'
29
+
26
30
  Metrics/ParameterLists:
27
31
  Exclude:
28
32
  - 'lib/moxml/materializer.rb'
data/Rakefile CHANGED
@@ -190,7 +190,12 @@ namespace :spec do
190
190
 
191
191
  desc "Run unit tests only"
192
192
  RSpec::Core::RakeTask.new(:unit) do |t|
193
- t.pattern = "spec/unit/**/*_spec.rb"
193
+ # Unit specs live under spec/moxml/ — the stale glob matched
194
+ # nothing (spec/unit/ holds only the rexml_isolated_test.rb
195
+ # entity-era investigation artifact), so this leg ran zero
196
+ # examples and hid the moxml suite from the fast gate.
197
+ t.pattern = "spec/moxml/**/*_spec.rb"
198
+ t.exclude_pattern = "spec/moxml/adapter/**/*_spec.rb"
194
199
  end
195
200
 
196
201
  desc "Run adapter tests only"
@@ -1893,6 +1893,54 @@ module Moxml
1893
1893
  installed
1894
1894
  end
1895
1895
 
1896
+ # moxml#335: the engine's document resolution can mis-report
1897
+ # pool ownership under spec-order state (the #1242 TLS-memo
1898
+ # family), so a plain add can -1 for ANY node kind — not just
1899
+ # elements and text. Elements rebuild structurally (the #376
1900
+ # contract); every other kind carries no cross-document
1901
+ # identity worth preserving, so reconstruct it in the parent's
1902
+ # document through the binding's create faces.
1903
+ def contains_foreign_child?(parent_nn, children)
1904
+ parent_doc = NN_DOCUMENT.bind_call(parent_nn)
1905
+ return false if parent_doc.nil?
1906
+
1907
+ children.any? do |child|
1908
+ child_doc = NN_DOCUMENT.bind_call(child)
1909
+ child_doc && !child_doc.equal?(parent_doc)
1910
+ end
1911
+ end
1912
+
1913
+ def rebuild_foreign_child(parent_nn, child_nn)
1914
+ doc = NN_DOCUMENT.bind_call(parent_nn)
1915
+ rebuilt =
1916
+ case child_nn.node_type
1917
+ when :element
1918
+ return structural_adopt(parent_nn, child_nn)
1919
+ when :text
1920
+ doc.create_text_node(child_nn.text)
1921
+ when :comment
1922
+ doc.create_comment(child_nn.text)
1923
+ when :cdata
1924
+ doc.create_cdata(child_nn.text)
1925
+ when :processing_instruction, :pi
1926
+ doc.create_processing_instruction(
1927
+ child_nn.target, child_nn.content.to_s
1928
+ )
1929
+ else
1930
+ raise Moxml::DocumentStructureError.new(
1931
+ "cannot rebuild a #{child_nn.node_type} node",
1932
+ )
1933
+ end
1934
+ # The raw engine add rejects binding-family children —
1935
+ # append through the FFI face like structural_adopt.
1936
+ ::Leptris::XML::FFI.check_status(
1937
+ ::Leptris::XML::FFI.leptris_element_append_child(
1938
+ ::FFI::Pointer.new(parent_nn.address), rebuilt.c_ptr
1939
+ ),
1940
+ )
1941
+ rebuilt
1942
+ end
1943
+
1896
1944
  def copy_binding_subtree(doc, src)
1897
1945
  sub = doc.create_element(src.name.to_s)
1898
1946
  (src.respond_to?(:attributes) ? src.attributes : {}).each do |k, v|
@@ -2107,24 +2155,15 @@ module Moxml
2107
2155
  child = doc.create_text_node(child) if child.is_a?(String)
2108
2156
  # The engine's document resolution can mis-report pool
2109
2157
  # ownership under spec-order state (the #1242 TLS-memo
2110
- # family) — an -1 rejection falls back to the structural
2111
- # adopt for elements rather than raising through the flow.
2158
+ # family) — an -1 rejection rebuilds the child in the
2159
+ # parent's document for every reconstructible kind
2160
+ # (moxml#335), rather than raising through the flow.
2112
2161
  begin
2113
2162
  return NN_ADD_CHILD.bind_call(parent, child)
2114
2163
  rescue ::Leptris::XML::Error, ::RuntimeError => e
2115
2164
  raise unless e.message.include?("append_child failed")
2116
- raise unless %i[element text].include?(child.node_type)
2117
2165
 
2118
- if child.node_type == :element
2119
- structural_adopt(parent, child)
2120
- else
2121
- # A text node carries no cross-document identity worth
2122
- # preserving: rebuild it in the parent's document
2123
- NN_ADD_CHILD.bind_call(
2124
- parent,
2125
- doc.create_text_node(child.text),
2126
- )
2127
- end
2166
+ rebuild_foreign_child(parent, child)
2128
2167
  end
2129
2168
  end
2130
2169
 
@@ -2319,6 +2358,32 @@ module Moxml
2319
2358
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
2320
2359
 
2321
2360
  def replace_children(node, new_children)
2361
+ # Cross-pool children: the binding's bulk face adopts per
2362
+ # node and fails on mixed kinds (inner_xml= with
2363
+ # text+elements died with Memory allocation failed —
2364
+ # pre-existing). Rebuild foreign children in this pool
2365
+ # after the clear — fragment children carry no identity
2366
+ # worth preserving; same-document children keep the plain
2367
+ # add, in order.
2368
+ if NATIVE_MUTATIONS_COHERENT && node.is_a?(NN) &&
2369
+ new_children.all?(NN) &&
2370
+ contains_foreign_child?(node, new_children)
2371
+ ::Leptris::XML::FFI.check_status(
2372
+ ::Leptris::XML::FFI.leptris_element_remove_children(
2373
+ ::FFI::Pointer.new(node.address),
2374
+ ),
2375
+ )
2376
+ node_doc = NN_DOCUMENT.bind_call(node)
2377
+ new_children.each do |child|
2378
+ child_doc = NN_DOCUMENT.bind_call(child)
2379
+ if child_doc && !child_doc.equal?(node_doc)
2380
+ rebuild_foreign_child(node, child)
2381
+ else
2382
+ NN_ADD_CHILD.bind_call(node, child)
2383
+ end
2384
+ end
2385
+ return node
2386
+ end
2322
2387
  if NATIVE_READ_LAYER
2323
2388
  node = to_binding(node)
2324
2389
  new_children = new_children.map { |child| to_binding(child) }
data/lib/moxml/element.rb CHANGED
@@ -400,14 +400,17 @@ module Moxml
400
400
  end
401
401
 
402
402
  def inner_xml=(xml)
403
- wrapper = "_moxml_inner_#{Process.pid}_#{object_id}"
404
- doc = context.parse("<#{wrapper}>#{xml}</#{wrapper}>")
403
+ # Fragment parse, not a wrapper element around interpolated
404
+ # input (TODO.refactor/03): the fragment face is the same one
405
+ # add_child(String) uses, so semantics match and no wrapper
406
+ # name can collide with content.
407
+ nodes = context.parse_fragment(xml).to_a
405
408
  @text = nil
406
409
  # Whole-list replacement rides the children generation like
407
410
  # children= does (moxml#310) — text and elements memos across
408
411
  # wrappers recompute on next read.
409
412
  context.bump_children_generation
410
- adapter.replace_children(@native, doc.root.children.map(&:native))
413
+ adapter.replace_children(@native, nodes.map(&:native))
411
414
  invalidate_children_cache!
412
415
  end
413
416
 
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.112"
4
+ VERSION = "0.5.114"
5
5
  end
@@ -89,6 +89,25 @@ RSpec.shared_examples "Moxml::Element" do
89
89
  expect(element.children.first.name).to eq("child")
90
90
  expect(element.children.first.text).to eq("text")
91
91
  end
92
+
93
+ it "sets inner XML with mixed top-level nodes" do
94
+ # Structure, not serialization spelling: Ox renders bare
95
+ # text as <s> nodes and rexml/oga expand empty tags.
96
+ element.inner_xml = "a<c1/>b<c2 v='1'/>c"
97
+ kids = element.children
98
+ expect(
99
+ kids.map { |c| c.element? ? :element : :content },
100
+ ).to eq(%i[content element content element content])
101
+ expect(kids[0].text).to eq("a")
102
+ expect(kids[2].text).to eq("b")
103
+ expect(kids[4].text).to eq("c")
104
+ expect(kids[3]["v"]).to eq("1")
105
+ end
106
+
107
+ it "sets inner XML containing element-like text and entities" do
108
+ element.inner_xml = "<c>&lt;/element&gt; &amp; &#65;</c>"
109
+ expect(element.children.first.text).to eq("</element> & A")
110
+ end
92
111
  end
93
112
 
94
113
  describe "node manipulation" do
@@ -10,6 +10,7 @@ require "spec_helper"
10
10
  # names, doc-owned).
11
11
  RSpec.describe "leptris cross-document adoption" do
12
12
  let(:ctx) { Moxml.new(:leptris) }
13
+ let(:ad) { Moxml::Adapter::Leptris }
13
14
 
14
15
  it "add_child lands the adopted copy in the tree" do
15
16
  doc = ctx.parse("<r><a/></r>")
@@ -100,4 +101,49 @@ RSpec.describe "leptris cross-document adoption" do
100
101
  expect(mid.root.to_xml).not_to include("<deep")
101
102
  end
102
103
  end
104
+
105
+ # moxml#335: the -1 fallback must not re-raise by node kind — the
106
+ # engine's document resolution can mis-report pool ownership for
107
+ # ANY child (the #1242 TLS-memo family), and only elements have a
108
+ # cross-document identity worth a structural rebuild. The helper
109
+ # is specced directly: the engine failure is CI-only, so forcing
110
+ # the -1 locally is not reproducible.
111
+ context "when the add_child -1 rebuild fallback fires" do
112
+ it "rebuilds comments in the parent's document" do
113
+ doc = ctx.parse("<r/>")
114
+ other = ctx.parse("<other><!-- note --></other>")
115
+ ad.rebuild_foreign_child(doc.root.native, other.root.children.first.native)
116
+ expect(doc.root.to_xml).to include("<!-- note -->")
117
+ expect(other.root.to_xml).to include("<!-- note -->")
118
+ end
119
+
120
+ it "rebuilds CDATA in the parent's document" do
121
+ doc = ctx.parse("<r/>")
122
+ other = ctx.parse("<other><![CDATA[x < y]]></other>")
123
+ ad.rebuild_foreign_child(doc.root.native, other.root.children.first.native)
124
+ expect(doc.root.to_xml).to include("<![CDATA[x < y]]>")
125
+ end
126
+
127
+ it "rebuilds processing instructions in the parent's document" do
128
+ doc = ctx.parse("<r/>")
129
+ other = ctx.parse("<other><?tgt data='1'?></other>")
130
+ ad.rebuild_foreign_child(doc.root.native, other.root.children.first.native)
131
+ expect(doc.root.to_xml).to include("<?tgt data='1'?>")
132
+ end
133
+
134
+ it "rebuilds text in the parent's document" do
135
+ doc = ctx.parse("<r/>")
136
+ other = ctx.parse("<other>t</other>")
137
+ ad.rebuild_foreign_child(doc.root.native, other.root.children.first.native)
138
+ expect(doc.root.to_xml).to include(">t<")
139
+ end
140
+
141
+ it "rebuilds elements structurally" do
142
+ doc = ctx.parse("<r/>")
143
+ other = ctx.parse("<other><e k='v'>t</e></other>")
144
+ ad.rebuild_foreign_child(doc.root.native, other.root.children.first.native)
145
+ expect(doc.at("//r/e")["k"]).to eq("v")
146
+ expect(doc.at("//r/e").text).to eq("t")
147
+ end
148
+ end
103
149
  end
@@ -168,9 +168,10 @@ RSpec.describe Moxml::Node do
168
168
  end
169
169
  end
170
170
 
171
- it "raises for node types without a path representation" do
171
+ it "locates non-element nodes through typed node tests" do
172
172
  text = doc.root.children.first.children.first
173
- expect { text.path }.to raise_error(Moxml::NotImplementedError, /element and document/)
173
+ expect(text.path).to end_with("/text()")
174
+ expect(doc.at_xpath(text.path)).to eq(text)
174
175
  end
175
176
  end
176
177
 
@@ -3,12 +3,18 @@
3
3
  require "spec_helper"
4
4
  require "moxml/adapter/shared_examples/adapter_contract"
5
5
 
6
- RSpec.describe Moxml::Adapter::Oga, if: RUBY_ENGINE == "opal" do
7
- around do |example|
8
- Moxml.with_config(:oga, true, "UTF-8") do
9
- example.run
6
+ # The describe resolves the adapter constant at load time — under
7
+ # MRI nothing loads the Opal compat shim, so the whole describe must
8
+ # sit behind the engine check (the `if:` metadata alone still
9
+ # evaluates the constant reference).
10
+ if RUBY_ENGINE == "opal"
11
+ RSpec.describe Moxml::Adapter::Oga do
12
+ around do |example|
13
+ Moxml.with_config(:oga, true, "UTF-8") do
14
+ example.run
15
+ end
10
16
  end
11
- end
12
17
 
13
- it_behaves_like "xml adapter"
18
+ it_behaves_like "xml adapter"
19
+ end
14
20
  end
@@ -164,7 +164,11 @@ RSpec.describe "SAX/DOM entity parity" do
164
164
  # double-escaping on round-trip.
165
165
  doc = ctx.create_document
166
166
  el = doc.create_element("doc")
167
- el.add_child("a & b")
167
+ # Programmatically-built: a literal Text node, no markup parse.
168
+ # (add_child with a String operand is MARKUP since the nokogiri
169
+ # compat round — a bare "&" is ill-formed there and each
170
+ # engine recovers differently.)
171
+ el.add_child(doc.create_text("a & b"))
168
172
  doc.add_child(el)
169
173
 
170
174
  serialized = doc.to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
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.112
4
+ version: 0.5.114
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-06 00:00:00.000000000 Z
11
+ date: 2026-10-07 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