moxml 0.5.106 → 0.5.108

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: 28a815d79bd0bc2f356a01f0a4f5337e111ab6bce63f7e8c2f7247efac096b4c
4
- data.tar.gz: b0a75eb438464fe9327fdb0420f14be0e8b19fea4252fe8abc995e295a6c6449
3
+ metadata.gz: fff38acca654943d1500e6647de3b62fadbb6919e73a1a655c84718a47a08d20
4
+ data.tar.gz: cb163aac66d87cb7bb3140bfce8f682791b4cb8430bb435219d3d30141e5ada1
5
5
  SHA512:
6
- metadata.gz: 33363ebc463f8e46fbd955ad3adb8f43b01775c27cc72ddaf356e62761e9a7b1917c1c15de36716806a1c306c111368fa0705ec1e37a11fd386f6183c274f291
7
- data.tar.gz: aef109c340cdcb4f30a76f87ef05e2568091e01c3dcf815fa2c945ff1cc49a961a0c36e1541f32873cca0226af2bd43a019c7cdac0c5f89b195558c2090a621a
6
+ metadata.gz: c8ce340193f8f168f5fc06cc12191478dddd56e0debb1e960c43aee2380613350ae3d44e972ba3d1a5084319c3c26902c3ee5aa14ad61c94af57ca259045b1db
7
+ data.tar.gz: 771fdc938de78cb25a12830f1257f80c2387ee56916b29c9de46dfa3bb340a3179ed5876498c2c36b22ca4c870f3a70a14c495783aeca73ff68f0eaab59b42f1
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`,
@@ -650,8 +655,48 @@ root.add_child(title)
650
655
  puts doc.to_xml(indent: 2)
651
656
  ----
652
657
 
658
+ === Performance versus nokogiri
659
+
660
+ CPU-time medians (1,000-record catalog, 20 iterations) through the
661
+ moxml adapter layer on both sides — moxml + leptris 1.9.307 vs
662
+ moxml + nokogiri:
663
+
664
+ [cols="2,1,4"]
665
+ |===
666
+ |Operation |leptris vs nokogiri |Notes
667
+
668
+ |parse
669
+ |3.27x
670
+ |Engine pool-carved parse
671
+
672
+ |serialize
673
+ |3.38x
674
+ |
675
+
676
+ |XPath
677
+ |50x
678
+ |Native compiled-eval seams
679
+
680
+ |walk + read (`.text`, `[]`)
681
+ |1.90x
682
+ |C klass-mint wrappers + wrapper read memos
683
+
684
+ |warm re-read
685
+ |2.10x
686
+ |The memo family — zero string allocation per node
687
+
688
+ |build (`add_element`)
689
+ |4.24x
690
+ |C-mint construction + bulk attributes
691
+
692
+ |attribute pairs
693
+ |1.73x
694
+ |One C crossing, no Attribute objects
695
+ |===
696
+
653
697
  === Fast paths and diagnostics
654
698
 
699
+
655
700
  Read-only attribute listing without Attribute node objects — the
656
701
  walk-hot shape when only names and values are needed (leptris
657
702
  answers in one C crossing; document order, duplicates included):
@@ -663,7 +708,22 @@ element.attribute_pairs.each do |name, value|
663
708
  end
664
709
  ----
665
710
 
711
+ Wrapper read memos — `Element#text`, `Element#inner_text` and
712
+ `Attribute#value` memoize on the mutation clocks (`#321`): re-read
713
+ walks pay zero string allocation per node (a text write, child
714
+ mutation or attribute write retires the memo; bulk builders keep
715
+ their local-invalidation shape).
716
+
717
+ Nokogiri-compatible node surface — `<<` (strings parse as markup),
718
+ `Element#delete(name)`, `Element#children=` (whole-list batch
719
+ replacement; one crossing to clear, one to append on leptris),
720
+ `Element#elements`/`element_children` (engine-level element-only
721
+ filtering), plus `next=`/`previous=`, `add_first_child`,
722
+ `parent=`, `ancestors(sel)`, `NodeSet#-`/`to_ary`/`to_xml`,
723
+ `to_s`/`to_str`, `content=` and `default_namespace=`.
724
+
666
725
  Typed struct members on `Moxml::StructPlan` — the executor casts
726
+
667
727
  in C with no intermediate String (unparseable input degrades to
668
728
  the raw String):
669
729
 
@@ -1862,6 +1862,51 @@ module Moxml
1862
1862
  attr
1863
1863
  end
1864
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
+
1865
1910
  def actual_native(child_native, parent_native)
1866
1911
  if NATIVE_DOC_PARTS && child_native.is_a?(CustomizedLeptris::Doctype) &&
1867
1912
  parent_native.is_a?(::Leptris::XML::Document)
@@ -1921,9 +1966,35 @@ module Moxml
1921
1966
  return child
1922
1967
  end
1923
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
+
1924
1984
  doc = NN_DOCUMENT.bind_call(parent)
1925
1985
  child = doc.create_text_node(child) if child.is_a?(String)
1926
- 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
1927
1998
  end
1928
1999
 
1929
2000
  child = parent.document.create_text_node(child) if child.is_a?(String)
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.106"
4
+ VERSION = "0.5.108"
5
5
  end
@@ -155,6 +155,19 @@ module Moxml
155
155
 
156
156
  private
157
157
 
158
+ # After an E/e: a digit, or a sign followed by a digit. Anything
159
+ # else leaves the E as part of a name (1o vs 1e ambiguity guard).
160
+ def lookahead_after_e_is_digit_sign?
161
+ next_char = @expression[@position + 1]
162
+ return true if /\d/.match?(next_char)
163
+
164
+ if /[+-]/.match?(next_char)
165
+ return @expression[@position + 2]&.match?(/\d/) || false
166
+ end
167
+
168
+ false
169
+ end
170
+
158
171
  # Get current character
159
172
  #
160
173
  # @return [String, nil] Current character or nil if at end
@@ -259,6 +272,22 @@ module Moxml
259
272
  end
260
273
  end
261
274
 
275
+ # Exponent part (libxml2/nokogiri extension): digits [Ee] [+|-]
276
+ # digits. Without it, 1E+1 tokenized as 1E followed by + 1 and
277
+ # predicates raised "Expected ']' after predicate".
278
+ if @position < @length && current_char =~ /[Ee]/ &&
279
+ @position + 1 < @length && lookahead_after_e_is_digit_sign?
280
+ value += current_char
281
+ advance
282
+ value += current_char if /[+-]/.match?(current_char)
283
+ advance if /[+-]/.match?(current_char)
284
+
285
+ while @position < @length && current_char =~ /\d/
286
+ value += current_char
287
+ advance
288
+ end
289
+ end
290
+
262
291
  add_token(:number, value, start_pos)
263
292
  end
264
293
 
@@ -451,8 +451,10 @@ module Moxml
451
451
  elsif match?(:number)
452
452
  value = current_value
453
453
  advance
454
- # Convert string to actual numeric value
455
- numeric_value = value.include?(".") ? value.to_f : value.to_i
454
+ # Convert string to actual numeric value. Exponent
455
+ # literals must go through to_f — to_i truncates at the E
456
+ # ("1E+1" became 1).
457
+ numeric_value = value.match?(/[.Ee]/) ? value.to_f : value.to_i
456
458
  return AST::Node.number(numeric_value)
457
459
  elsif match?(:dollar)
458
460
  advance
@@ -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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Signed exponent literals (libxml2/nokogiri extension): the lexer
6
+ # consumed exponent digits but not their sign, so 1E+1 tokenized as
7
+ # 1E + 1 and predicates raised "Expected ']'"; and the parser's
8
+ # numeric conversion ran to_i on dotless literals, so 2E3 evaluated
9
+ # as 2. Both fixed; the engine now matches nokogiri on every shape.
10
+ RSpec.describe "XPath exponent number literals" do
11
+ %i[nokogiri ox].each do |adapter|
12
+ it "evaluates signed exponents on #{adapter}" do
13
+ ctx = Moxml.new(adapter)
14
+ doc = ctx.parse("<r><x v='2000'/><x v='5'/></r>")
15
+ expect(doc.xpath("//x[@v > 1E+1]").size).to eq(1)
16
+ expect(doc.xpath("//x[@v = 2E3]").size).to eq(1)
17
+ expect(doc.xpath("//x[@v > 5.2E-3]").size).to eq(2)
18
+ expect(doc.xpath("//x[@v < 1e1]").size).to eq(1)
19
+ end
20
+ end
21
+
22
+ it "tokenizes exponent literals as one number" do
23
+ tokens = Moxml::XPath::Lexer.new("1E+1 2E3 5.2E-3 1e2").tokenize
24
+ numbers = tokens.select { |t| t.first == :number }.map { |t| t[1] }
25
+ expect(numbers).to eq(["1E+1", "2E3", "5.2E-3", "1e2"])
26
+ end
27
+
28
+ it "parses exponent literals at full precision" do
29
+ ast = Moxml::XPath::Parser.parse("1E+1")
30
+ literal = ast
31
+ literal = literal.children.first while literal.children.any? { |c| c.respond_to?(:type) }
32
+ expect(literal.value).to eq(10.0)
33
+ end
34
+ end
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.106
4
+ version: 0.5.108
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-04 00:00:00.000000000 Z
11
+ date: 2026-10-06 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
@@ -556,6 +557,7 @@ files:
556
557
  - spec/moxml/xpath/functions/special_functions_spec.rb
557
558
  - spec/moxml/xpath/functions/string_functions_spec.rb
558
559
  - spec/moxml/xpath/lexer_spec.rb
560
+ - spec/moxml/xpath/number_literals_spec.rb
559
561
  - spec/moxml/xpath/parser_integration_spec.rb
560
562
  - spec/moxml/xpath/parser_spec.rb
561
563
  - spec/moxml/xpath/ruby/generator_spec.rb