moxml 0.5.99 → 0.5.101
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 +4 -4
- data/lib/moxml/adapter/base.rb +8 -0
- data/lib/moxml/adapter/leptris/document_parts.rb +1 -1
- data/lib/moxml/adapter/leptris.rb +51 -7
- data/lib/moxml/cdata.rb +3 -0
- data/lib/moxml/comment.rb +3 -0
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/document.rb +8 -0
- data/lib/moxml/node.rb +75 -0
- data/lib/moxml/text.rb +3 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_append_gc_spec.rb +14 -0
- data/spec/moxml/adapter/leptris_binding_cap_spec.rb +29 -0
- data/spec/moxml/adapter/leptris_xpath_identity_spec.rb +49 -0
- data/spec/moxml/at_search_spec.rb +85 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9109c62e1dc81916376846aa35756d02e7caa0caedbf96937c6c89fb99687389
|
|
4
|
+
data.tar.gz: cab100fc1e408a0c511d330c81bc5f2b71e8dc0f6fa30e2c1b7a7e094d1c81a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d041b4af64d6fe7d0d87d129ce35053da5405ba23b866e7dd3ef8c7237a21b73ff60d9baefafacf39781d5691e44b95ec295fcfe9eb54fa5e2a091ed01ea6ab7
|
|
7
|
+
data.tar.gz: 2f21edaf882c65e3229c617eb5df04e49b0198de3133d968e898fa9ec9218d34b52c5f165791804aafb328bae55db20fddc80c0976d596c686e66630bbd0293d
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -146,6 +146,14 @@ module Moxml
|
|
|
146
146
|
nil
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
# Canonical node identity: adapters carrying more than one
|
|
150
|
+
# Ruby representation over one C node answer the representation
|
|
151
|
+
# the tree walk mints, so every wrap path resolves one wrapper
|
|
152
|
+
# per logical node (moxml#311). Default: the node is canonical.
|
|
153
|
+
def canonical_native(node)
|
|
154
|
+
node
|
|
155
|
+
end
|
|
156
|
+
|
|
149
157
|
# Prefixed-attribute value fast path: adapters whose engine
|
|
150
158
|
# resolves expanded-name (uri, local) lookups natively answer
|
|
151
159
|
# the value here; others fall back to the resolver's
|
|
@@ -48,7 +48,7 @@ module Moxml
|
|
|
48
48
|
# equal?-based exclusion (canon's document-element skip)
|
|
49
49
|
# silently breaks (issue #219). Mint-on-miss converges
|
|
50
50
|
# both accessors on the same native object.
|
|
51
|
-
children.map! { |child|
|
|
51
|
+
children.map! { |child| native_twin(doc, child) } if NATIVE_READ_LAYER
|
|
52
52
|
children
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -168,6 +168,18 @@ module Moxml
|
|
|
168
168
|
BINDING_OF_CAP = 8_192
|
|
169
169
|
|
|
170
170
|
class << self
|
|
171
|
+
# WeakMap has no #clear — 0.5.100's valve crashed every parse
|
|
172
|
+
# crossing the cap on 3.4+ (NoMethodError). Replace the
|
|
173
|
+
# registry wholesale, type-preserving: the WeakMap flavor
|
|
174
|
+
# stays weak, the strong Hash keeps compare_by_identity.
|
|
175
|
+
def fresh_node_registry
|
|
176
|
+
if Context::WEAK_WRAPPERS
|
|
177
|
+
ObjectSpace::WeakMap.new
|
|
178
|
+
else
|
|
179
|
+
{}.compare_by_identity
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
171
183
|
# native -> bridged binding node. The bridge object is
|
|
172
184
|
# identity-stable for the native's lifetime (the binding's
|
|
173
185
|
# per-document wrap cache), so repeat bridges — the write
|
|
@@ -187,7 +199,7 @@ module Moxml
|
|
|
187
199
|
doc = doc_for(node)
|
|
188
200
|
ptr = ::FFI::Pointer.new(node.address)
|
|
189
201
|
bridged = ::Leptris::XML::Node.wrap(ptr, doc)
|
|
190
|
-
@binding_of
|
|
202
|
+
@binding_of = fresh_node_registry if @binding_of.size >= BINDING_OF_CAP
|
|
191
203
|
@binding_of[node] = bridged unless bridged.nil?
|
|
192
204
|
bridged
|
|
193
205
|
end
|
|
@@ -428,7 +440,7 @@ module Moxml
|
|
|
428
440
|
end
|
|
429
441
|
|
|
430
442
|
def record_native_doc(root_native, doc)
|
|
431
|
-
@native_doc_roots
|
|
443
|
+
@native_doc_roots = fresh_node_registry if @native_doc_roots.size >= NATIVE_DOC_ROOTS_CAP
|
|
432
444
|
@native_doc_roots[root_native] = doc
|
|
433
445
|
end
|
|
434
446
|
|
|
@@ -1511,7 +1523,7 @@ module Moxml
|
|
|
1511
1523
|
# accessor converges on one native per node (issue #219).
|
|
1512
1524
|
# Binding natives (Attribute/Attr, synthetic wrappers) and
|
|
1513
1525
|
# docless nodes pass through unchanged.
|
|
1514
|
-
def
|
|
1526
|
+
def native_twin(doc, node)
|
|
1515
1527
|
return node unless node.is_a?(::Leptris::XML::Element)
|
|
1516
1528
|
|
|
1517
1529
|
cache = doc.native_cache
|
|
@@ -1625,7 +1637,7 @@ module Moxml
|
|
|
1625
1637
|
# Through the same address-keyed cache as the children
|
|
1626
1638
|
# path: NativeNode.from alone mints a fresh object per
|
|
1627
1639
|
# call on 1.9.163.x, splitting the wrappers (issue #219).
|
|
1628
|
-
native =
|
|
1640
|
+
native = native_twin(document, r)
|
|
1629
1641
|
record_native_doc(native, document)
|
|
1630
1642
|
return native
|
|
1631
1643
|
end
|
|
@@ -1686,8 +1698,8 @@ module Moxml
|
|
|
1686
1698
|
# convergence (#219) hands every accessor — children
|
|
1687
1699
|
# included — the SAME native, so the wrapper identity
|
|
1688
1700
|
# holds across later traversals.
|
|
1689
|
-
|
|
1690
|
-
|
|
1701
|
+
native_twin(doc,
|
|
1702
|
+
::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
|
|
1691
1703
|
end
|
|
1692
1704
|
|
|
1693
1705
|
def attributes(element)
|
|
@@ -1793,8 +1805,14 @@ module Moxml
|
|
|
1793
1805
|
parent_doc = parent.is_a?(NN) ? NN_DOCUMENT.bind_call(parent) : parent.document
|
|
1794
1806
|
return if parent_doc.nil? || child_doc.equal?(parent_doc)
|
|
1795
1807
|
|
|
1808
|
+
# moxml#308: one source document's children attach
|
|
1809
|
+
# node-by-node — dedup by identity or the pin list grows
|
|
1810
|
+
# linearly with append count (GB-scale malloc on
|
|
1811
|
+
# large-document hydration). Distinct owners stay few.
|
|
1796
1812
|
owner = child_doc.is_a?(NN) ? to_binding(child_doc) : child_doc
|
|
1797
1813
|
docs = attachments.get(parent_doc, :adopted_docs) || []
|
|
1814
|
+
return if docs.include?(owner)
|
|
1815
|
+
|
|
1798
1816
|
docs << owner
|
|
1799
1817
|
attachments.set(parent_doc, :adopted_docs, docs)
|
|
1800
1818
|
end
|
|
@@ -1833,6 +1851,7 @@ module Moxml
|
|
|
1833
1851
|
node = to_binding(node)
|
|
1834
1852
|
new_node = to_binding(new_node)
|
|
1835
1853
|
end
|
|
1854
|
+
pin_foreign_owner_document(node, new_node)
|
|
1836
1855
|
# A PI inserted before the root lives at document level in
|
|
1837
1856
|
# libleptris's model, not in the element tree.
|
|
1838
1857
|
if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
|
|
@@ -1858,6 +1877,7 @@ module Moxml
|
|
|
1858
1877
|
node = to_binding(node)
|
|
1859
1878
|
new_node = to_binding(new_node)
|
|
1860
1879
|
end
|
|
1880
|
+
pin_foreign_owner_document(node, new_node)
|
|
1861
1881
|
return node.add_next_sibling(new_node) if node.is_a?(::Leptris::XML::Element)
|
|
1862
1882
|
|
|
1863
1883
|
# Non-element receivers (Text/Comment/CDATA): the binding's
|
|
@@ -2112,6 +2132,29 @@ module Moxml
|
|
|
2112
2132
|
engine_xpath(node, expression, namespaces)
|
|
2113
2133
|
end
|
|
2114
2134
|
|
|
2135
|
+
# Canonical node identity (moxml#311): the native layer and
|
|
2136
|
+
# the binding layer hold separate Ruby objects over one C
|
|
2137
|
+
# node, and only the native cache carries the tree-walk
|
|
2138
|
+
# identity the @parent_node invalidation chain rides on.
|
|
2139
|
+
# Resolve binding-layer nodes (xpath results, engine results)
|
|
2140
|
+
# to the walked instance when one exists — a memoized
|
|
2141
|
+
# children holder implies a prior walk, so a cache miss means
|
|
2142
|
+
# no holder exists and the binding node is safe as-is.
|
|
2143
|
+
def canonical_native(node)
|
|
2144
|
+
return node unless NATIVE_READ_LAYER
|
|
2145
|
+
return node if node.is_a?(::Leptris::XML::NativeNode)
|
|
2146
|
+
return node unless node.is_a?(::Leptris::XML::Node)
|
|
2147
|
+
|
|
2148
|
+
doc = node.document
|
|
2149
|
+
return node if doc.nil?
|
|
2150
|
+
|
|
2151
|
+
# Only a contract-carrying hit is the walked canonical —
|
|
2152
|
+
# plain NN twins (entity references, #219 miss-mints) have
|
|
2153
|
+
# no Identity class and keep the binding representation.
|
|
2154
|
+
hit = doc.native_cache[node.c_ptr.address]
|
|
2155
|
+
hit.is_a?(::Moxml::Node) ? hit : node
|
|
2156
|
+
end
|
|
2157
|
+
|
|
2115
2158
|
def at_xpath(node, expression, namespaces = {})
|
|
2116
2159
|
native = native_xpath(node, expression, namespaces, first_only: true)
|
|
2117
2160
|
return native unless native.nil?
|
|
@@ -2188,7 +2231,8 @@ module Moxml
|
|
|
2188
2231
|
# and frees in one C dispatch — no NodeSet container, no
|
|
2189
2232
|
# AutoPointer (leptris-ruby TODO.perf/16). The module
|
|
2190
2233
|
# object back means a scalar, whose wrap (and free)
|
|
2191
|
-
# stays with wrap_xpath_first_result.
|
|
2234
|
+
# stays with wrap_xpath_first_result. Identity resolution
|
|
2235
|
+
# happens at the at_xpath boundary (bridge_xpath_node).
|
|
2192
2236
|
return ::Leptris::XML::Searchable.wrap_xpath_first_result(
|
|
2193
2237
|
document, result_ptr
|
|
2194
2238
|
)
|
data/lib/moxml/cdata.rb
CHANGED
data/lib/moxml/comment.rb
CHANGED
data/lib/moxml/context.rb
CHANGED
|
@@ -74,6 +74,18 @@ module Moxml
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Children-memo generation: bumped by every tree mutation so
|
|
78
|
+
# per-wrapper children memos can self-invalidate even when the
|
|
79
|
+
# same logical node is reachable through multiple wrapper
|
|
80
|
+
# representations (native vs binding duality).
|
|
81
|
+
def children_generation
|
|
82
|
+
@children_generation ||= 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bump_children_generation
|
|
86
|
+
@children_generation = children_generation + 1
|
|
87
|
+
end
|
|
88
|
+
|
|
77
89
|
def namespace_scope_generation
|
|
78
90
|
@namespace_scope_generation
|
|
79
91
|
end
|
data/lib/moxml/document.rb
CHANGED
|
@@ -113,6 +113,14 @@ module Moxml
|
|
|
113
113
|
Wrappers::EntityReference.new(native, context)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# Nokogiri-compatible: Document#name returns "document"
|
|
117
|
+
def name
|
|
118
|
+
"document"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Nokogiri-compatible: renaming a document node is a no-op
|
|
122
|
+
def name=(_value); end
|
|
123
|
+
|
|
116
124
|
def add_child(node)
|
|
117
125
|
node = prepare_node(node)
|
|
118
126
|
|
data/lib/moxml/node.rb
CHANGED
|
@@ -74,6 +74,11 @@ module Moxml
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def children
|
|
77
|
+
# Generation-checked memo: mutations bump the context counter, so
|
|
78
|
+
# this self-invalidates even when the same logical node is served
|
|
79
|
+
# by multiple wrapper representations (native vs binding duality).
|
|
80
|
+
@children = nil if @children_gen != context.children_generation
|
|
81
|
+
@children_gen = context.children_generation
|
|
77
82
|
@children ||= begin
|
|
78
83
|
# The wrapper's entity memo decides the marker split; the
|
|
79
84
|
# adapter would otherwise re-derive it per call (a C parent
|
|
@@ -100,6 +105,7 @@ module Moxml
|
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
def add_child(node)
|
|
108
|
+
context.bump_children_generation
|
|
103
109
|
node = prepare_node(node)
|
|
104
110
|
adapter.add_child(@native, node.native)
|
|
105
111
|
# Refresh native in case adapter changed identity (e.g., LibXML
|
|
@@ -116,20 +122,45 @@ module Moxml
|
|
|
116
122
|
end
|
|
117
123
|
|
|
118
124
|
def add_previous_sibling(node)
|
|
125
|
+
context.bump_children_generation
|
|
119
126
|
node = prepare_node(node)
|
|
120
127
|
adapter.add_previous_sibling(@native, node.native)
|
|
128
|
+
# Invalidate the parent's memoized children list. The wrapper-side
|
|
129
|
+
# @parent_node link is only set when this node was yielded through a
|
|
130
|
+
# parent-aware NodeSet; nodes obtained via at()/xpath() carry no such
|
|
131
|
+
# link, so resolve the parent wrapper from the adapter instead.
|
|
132
|
+
parent&.invalidate_children_cache!
|
|
121
133
|
invalidate_parent_children_cache!
|
|
122
134
|
self
|
|
123
135
|
end
|
|
124
136
|
|
|
137
|
+
# Nokogiri-compatible assignment forms: node.next = / node.previous =.
|
|
138
|
+
# String operands are parsed as XML fragments, like Nokogiri.
|
|
139
|
+
def next=(node)
|
|
140
|
+
add_next_sibling(sibling_operand(node))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def previous=(node)
|
|
144
|
+
add_previous_sibling(sibling_operand(node))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def sibling_operand(node)
|
|
148
|
+
return node unless node.is_a?(String)
|
|
149
|
+
|
|
150
|
+
context.parse_fragment(node).first
|
|
151
|
+
end
|
|
152
|
+
|
|
125
153
|
def add_next_sibling(node)
|
|
154
|
+
context.bump_children_generation
|
|
126
155
|
node = prepare_node(node)
|
|
127
156
|
adapter.add_next_sibling(@native, node.native)
|
|
157
|
+
parent&.invalidate_children_cache!
|
|
128
158
|
invalidate_parent_children_cache!
|
|
129
159
|
self
|
|
130
160
|
end
|
|
131
161
|
|
|
132
162
|
def remove
|
|
163
|
+
context.bump_children_generation
|
|
133
164
|
invalidate_parent_children_cache!
|
|
134
165
|
adapter.remove(@native)
|
|
135
166
|
invalidate_children_cache!
|
|
@@ -143,6 +174,7 @@ module Moxml
|
|
|
143
174
|
def invalidate_namespace_cache!; end
|
|
144
175
|
|
|
145
176
|
def replace(node)
|
|
177
|
+
context.bump_children_generation
|
|
146
178
|
node = prepare_node(node)
|
|
147
179
|
invalidate_parent_children_cache!
|
|
148
180
|
adapter.replace(@native, node.native)
|
|
@@ -215,6 +247,42 @@ module Moxml
|
|
|
215
247
|
context)
|
|
216
248
|
end
|
|
217
249
|
|
|
250
|
+
# Nokogiri-compatible sugar: at/search/at_css/css. Nokogiri's
|
|
251
|
+
# at/search auto-detect XPath vs CSS; the XPath-first fallback
|
|
252
|
+
# covers the dominant caller shape (XPath expression strings)
|
|
253
|
+
# while css/at_css serve stylesheet-style selectors.
|
|
254
|
+
def at(expression, namespaces = {})
|
|
255
|
+
at_css_or_xpath(expression, namespaces)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def search(expression, namespaces = {})
|
|
259
|
+
xpath(expression, namespaces)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def css(_expression)
|
|
263
|
+
raise Moxml::NotImplementedError,
|
|
264
|
+
"CSS selectors are not supported; use search with XPath"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def at_css(_expression)
|
|
268
|
+
raise Moxml::NotImplementedError,
|
|
269
|
+
"CSS selectors are not supported; use at with XPath"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def at_css_or_xpath(expression, namespaces = {})
|
|
273
|
+
at_xpath(expression, namespaces)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Nokogiri-compatible depth-first traversal: yields self, then
|
|
277
|
+
# children recursively (document-order visitor).
|
|
278
|
+
def traverse(&block)
|
|
279
|
+
return to_enum(:traverse) unless block
|
|
280
|
+
|
|
281
|
+
yield self
|
|
282
|
+
children.each { |child| child.traverse(&block) }
|
|
283
|
+
self
|
|
284
|
+
end
|
|
285
|
+
|
|
218
286
|
# Convenience find methods (aliases for xpath methods)
|
|
219
287
|
def find(xpath_expression, namespaces = {})
|
|
220
288
|
at_xpath(xpath_expression, namespaces)
|
|
@@ -591,6 +659,13 @@ module Moxml
|
|
|
591
659
|
def self.wrap_with(node, context, adapter)
|
|
592
660
|
return nil if node.nil?
|
|
593
661
|
|
|
662
|
+
# One C node may have several Ruby representations (leptris
|
|
663
|
+
# native/binding duality, moxml#311); every wrap resolves to
|
|
664
|
+
# the canonical one so memo holders share one invalidation
|
|
665
|
+
# chain. A hit-only lookup: a memoized holder implies a prior
|
|
666
|
+
# walk, which is what populates the canonical map.
|
|
667
|
+
node = adapter.canonical_native(node)
|
|
668
|
+
|
|
594
669
|
# Cache-stable binding nodes carry their wrapper directly
|
|
595
670
|
# (#312): the binding's document-owned cache hands the SAME
|
|
596
671
|
# node object for the engine node's whole lifetime, so an
|
data/lib/moxml/text.rb
CHANGED
data/lib/moxml/version.rb
CHANGED
|
@@ -49,4 +49,18 @@ RSpec.describe "leptris cross-document append under GC pressure" do
|
|
|
49
49
|
expect(out).to include("<annex")
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
# moxml#308: the pin list must dedup by identity — one source
|
|
54
|
+
# document's children attach node-by-node, and without dedup the
|
|
55
|
+
# list grows linearly with append count (GB-scale malloc on
|
|
56
|
+
# large-document hydration; OOM at 43-75M slots).
|
|
57
|
+
it "pins each adopted document once regardless of append count" do
|
|
58
|
+
target = ctx.parse("<r/>")
|
|
59
|
+
frag = ctx.parse("<m>#{Array.new(300) { |i| "<c#{i}>x</c#{i}>" }.join}</m>")
|
|
60
|
+
frag.root.children.each { |c| target.root.add_child(c) }
|
|
61
|
+
|
|
62
|
+
docs = ctx.config.adapter.attachments.get(target.native.document, :adopted_docs)
|
|
63
|
+
expect(docs.size).to eq(1)
|
|
64
|
+
expect(target.root.to_xml).to include("<c299")
|
|
65
|
+
end
|
|
52
66
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# 0.5.100's cap valves called .clear on the registry — an
|
|
6
|
+
# ObjectSpace::WeakMap in weak-wrapper mode (Ruby 3.4+) has no
|
|
7
|
+
# #clear, so the first parse crossing 8,192 bridges crashed with
|
|
8
|
+
# NoMethodError. The valve replaces the registry wholesale now.
|
|
9
|
+
RSpec.describe "leptris binding registry cap valve" do
|
|
10
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
11
|
+
let(:doc) do
|
|
12
|
+
ctx.parse("<r>#{Array.new(8_300) { |i| "<e#{i}/>" }.join}</r>")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "bridges past the cap" do
|
|
16
|
+
children = doc.root.children.to_a
|
|
17
|
+
expect(children.size).to be > 8_192
|
|
18
|
+
|
|
19
|
+
bridged = children.map { |child| Moxml::Adapter::Leptris.to_binding(child.native) }
|
|
20
|
+
expect(bridged.compact.size).to eq(children.size)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "bridges stay identity-stable across the valve" do
|
|
24
|
+
children = doc.root.children.to_a
|
|
25
|
+
first_pass = children.map { |child| Moxml::Adapter::Leptris.to_binding(child.native) }
|
|
26
|
+
second_pass = children.map { |child| Moxml::Adapter::Leptris.to_binding(child.native) }
|
|
27
|
+
expect(first_pass.zip(second_pass).all? { |one, other| one.equal?(other) }).to be(true)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# moxml#311: the native and binding layers hold separate Ruby objects
|
|
6
|
+
# over one C node. An xpath-resolved node that failed to resolve to
|
|
7
|
+
# the tree-walk identity carried no parent link, so sibling inserts
|
|
8
|
+
# through it never invalidated the memoized children of wrappers
|
|
9
|
+
# handed out before the query — the insert serialized but stayed
|
|
10
|
+
# invisible through parent.children.
|
|
11
|
+
RSpec.describe "leptris xpath nodes resolve canonical identity" do
|
|
12
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
13
|
+
let(:doc) { ctx.parse("<r><a>t</a>\n<b/></r>") }
|
|
14
|
+
|
|
15
|
+
it "at_xpath returns the walked wrapper" do
|
|
16
|
+
root = doc.root
|
|
17
|
+
walked = root.children.first
|
|
18
|
+
expect(root.at_xpath("a")).to be(walked)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "xpath list results resolve canonical identity" do
|
|
22
|
+
root = doc.root
|
|
23
|
+
walked = root.children.first
|
|
24
|
+
expect(root.xpath("a").first).to be(walked)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "namespace-bound at_xpath resolves canonical identity" do
|
|
28
|
+
root = doc.root
|
|
29
|
+
walked = root.children.first
|
|
30
|
+
expect(root.at_xpath("a", "xmlns" => "http://x")).to be(walked)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "sibling insert through at_xpath resolves in parent.children" do
|
|
34
|
+
root = doc.root
|
|
35
|
+
root.children
|
|
36
|
+
a = root.at_xpath("a")
|
|
37
|
+
a.add_next_sibling(ctx.parse_fragment("<x/>").first)
|
|
38
|
+
expect(root.children.map(&:name)).to eq(%w[a x text b])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "sibling insert through the engine path resolves in parent.children" do
|
|
42
|
+
root = doc.root
|
|
43
|
+
root.children
|
|
44
|
+
walked = root.children.first
|
|
45
|
+
a = walked.at_xpath("self::a[parent::r]")
|
|
46
|
+
a.add_next_sibling(ctx.parse_fragment("<x/>").first)
|
|
47
|
+
expect(root.children.map(&:name)).to eq(%w[a x text b])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Nokogiri-compatible node sugar" do
|
|
6
|
+
describe "at/search" do
|
|
7
|
+
let(:doc) do
|
|
8
|
+
Moxml.parse(<<~XML)
|
|
9
|
+
<root xmlns="http://example.com/ns">
|
|
10
|
+
<item id="a"><name>one</name></item>
|
|
11
|
+
<item id="b"><name>two</name></item>
|
|
12
|
+
</root>
|
|
13
|
+
XML
|
|
14
|
+
end
|
|
15
|
+
let(:ns) { { "xmlns" => "http://example.com/ns" } }
|
|
16
|
+
|
|
17
|
+
it "at returns the first match like Nokogiri" do
|
|
18
|
+
node = doc.at("//xmlns:item", ns)
|
|
19
|
+
expect(node["id"]).to eq("a")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "at on document delegates to root search" do
|
|
23
|
+
expect(doc.at("//xmlns:name", ns).text).to eq("one")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "search returns all matches" do
|
|
27
|
+
expect(doc.search("//xmlns:item", ns).size).to eq(2)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "at returns nil when nothing matches" do
|
|
31
|
+
expect(doc.at("//xmlns:missing", ns)).to be_nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "at works on element nodes" do
|
|
35
|
+
item = doc.at("//xmlns:item[@id='b']", ns)
|
|
36
|
+
expect(item.at("xmlns:name", ns).text).to eq("two")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "Node#traverse" do
|
|
41
|
+
it "yields depth-first in document order from an element" do
|
|
42
|
+
doc = Moxml.parse("<r><a><b/></a><c/></r>")
|
|
43
|
+
names = []
|
|
44
|
+
doc.root.traverse { |n| names << n.name }
|
|
45
|
+
expect(names).to eq(%w[r a b c])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "yields the document first from a document traversal" do
|
|
49
|
+
doc = Moxml.parse("<r><a/></r>")
|
|
50
|
+
names = []
|
|
51
|
+
doc.traverse { |n| names << n.name }
|
|
52
|
+
expect(names).to eq(%w[document r a])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "Node#next=/previous=" do
|
|
57
|
+
it "insert a sibling after/before via assignment" do
|
|
58
|
+
doc = Moxml.parse("<r><a/><b/></r>")
|
|
59
|
+
a = doc.at("//a")
|
|
60
|
+
a.next = "<x/>"
|
|
61
|
+
expect(doc.root.children.map(&:name)).to eq(%w[a x b])
|
|
62
|
+
b = doc.at("//b")
|
|
63
|
+
b.previous = "<y/>"
|
|
64
|
+
expect(doc.root.children.map(&:name)).to eq(%w[a x y b])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "sibling inserts invalidate the parent children cache" do
|
|
69
|
+
it "next= is visible through parent.children when siblings are text" do
|
|
70
|
+
doc = Moxml.parse("<r><a/>\n<b/></r>")
|
|
71
|
+
doc.at("//a").next = "<x/>"
|
|
72
|
+
expect(doc.root.children.map(&:name)).to eq(%w[a x text b])
|
|
73
|
+
expect(doc.at("//x")).not_to be_nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "duality-proof: insert visible through a wrapper that memoized earlier" do
|
|
77
|
+
ctx = Moxml.new(:leptris)
|
|
78
|
+
doc = ctx.parse("<r><a/>\n<b/></r>")
|
|
79
|
+
doc.root.children.map(&:name) # memoize on the root wrapper
|
|
80
|
+
a = doc.at("//a") # possibly a different wrapper instance
|
|
81
|
+
a.next = "<x/>"
|
|
82
|
+
expect(doc.root.children.map(&:name)).to eq(%w[a x text b])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
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.
|
|
4
|
+
version: 0.5.101
|
|
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-
|
|
11
|
+
date: 2026-10-02 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
|
|
@@ -441,12 +441,14 @@ files:
|
|
|
441
441
|
- spec/moxml/adapter/entity_restoration_spec.rb
|
|
442
442
|
- spec/moxml/adapter/headed_ox_spec.rb
|
|
443
443
|
- spec/moxml/adapter/leptris_append_gc_spec.rb
|
|
444
|
+
- spec/moxml/adapter/leptris_binding_cap_spec.rb
|
|
444
445
|
- spec/moxml/adapter/leptris_doc_parts_spec.rb
|
|
445
446
|
- spec/moxml/adapter/leptris_parse_skips_spec.rb
|
|
446
447
|
- spec/moxml/adapter/leptris_record_driver_spec.rb
|
|
447
448
|
- spec/moxml/adapter/leptris_sax_dup_attr_spec.rb
|
|
448
449
|
- spec/moxml/adapter/leptris_sax_records_spec.rb
|
|
449
450
|
- spec/moxml/adapter/leptris_spec.rb
|
|
451
|
+
- spec/moxml/adapter/leptris_xpath_identity_spec.rb
|
|
450
452
|
- spec/moxml/adapter/libxml_internals_spec.rb
|
|
451
453
|
- spec/moxml/adapter/libxml_spec.rb
|
|
452
454
|
- spec/moxml/adapter/nokogiri_spec.rb
|
|
@@ -459,6 +461,7 @@ files:
|
|
|
459
461
|
- spec/moxml/adapter_spec.rb
|
|
460
462
|
- spec/moxml/allocation_benchmark_spec.rb
|
|
461
463
|
- spec/moxml/allocation_guard_spec.rb
|
|
464
|
+
- spec/moxml/at_search_spec.rb
|
|
462
465
|
- spec/moxml/attribute_resolver_spec.rb
|
|
463
466
|
- spec/moxml/attribute_spec.rb
|
|
464
467
|
- spec/moxml/builder_spec.rb
|