moxml 0.5.100 → 0.5.102
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/.github/workflows/dependent-tests.yml +7 -0
- data/.github/workflows/opal.yml +7 -0
- data/.github/workflows/rake.yml +7 -0
- data/.github/workflows/round-trip.yml +7 -0
- data/.github/workflows/windows.yml +7 -0
- data/lib/moxml/adapter/base.rb +28 -2
- data/lib/moxml/adapter/leptris/document_parts.rb +1 -1
- data/lib/moxml/adapter/leptris.rb +131 -12
- data/lib/moxml/adapter/libxml.rb +3 -2
- data/lib/moxml/adapter/nokogiri.rb +4 -0
- 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 +40 -0
- data/lib/moxml/element.rb +49 -0
- data/lib/moxml/node.rb +219 -23
- data/lib/moxml/node_set.rb +26 -0
- data/lib/moxml/text.rb +3 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +21 -18
- 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 +386 -0
- data/spec/moxml/xpath/union_spec.rb +42 -0
- metadata +6 -2
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
|
|
@@ -99,7 +104,105 @@ module Moxml
|
|
|
99
104
|
Moxml::Node.wrap(adapter.previous_sibling(@native), context)
|
|
100
105
|
end
|
|
101
106
|
|
|
107
|
+
# Nokogiri-compatible reader spellings.
|
|
108
|
+
alias next next_sibling
|
|
109
|
+
alias previous previous_sibling
|
|
110
|
+
|
|
111
|
+
# Nokogiri-compatible: nearest sibling elements, skipping text and
|
|
112
|
+
# comment nodes.
|
|
113
|
+
def next_element
|
|
114
|
+
s = next_sibling
|
|
115
|
+
s = s.next_sibling while s && !s.is_a?(Moxml::Element)
|
|
116
|
+
s
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def previous_element
|
|
120
|
+
s = previous_sibling
|
|
121
|
+
s = s.previous_sibling while s && !s.is_a?(Moxml::Element)
|
|
122
|
+
s
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Nokogiri-compatible append sugar: node << appends. Strings
|
|
126
|
+
# parse as fragments (markup, not literal text).
|
|
127
|
+
def <<(node)
|
|
128
|
+
add_child(node)
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Nokogiri-compatible: reparent — node.parent = new_parent moves
|
|
133
|
+
# this node (removing it from its current tree) under the target.
|
|
134
|
+
def parent=(new_parent)
|
|
135
|
+
remove
|
|
136
|
+
new_parent.add_child(self)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Insert ahead of the current first child (nokogiri-monkeypatch
|
|
140
|
+
# convention used across metanorma). Strings parse as fragments
|
|
141
|
+
# (the sibling_operand convention).
|
|
142
|
+
def add_first_child(node)
|
|
143
|
+
if first_child
|
|
144
|
+
first_child.add_previous_sibling(node)
|
|
145
|
+
else
|
|
146
|
+
add_child(node)
|
|
147
|
+
end
|
|
148
|
+
self
|
|
149
|
+
end
|
|
150
|
+
|
|
102
151
|
def add_child(node)
|
|
152
|
+
# Nokogiri semantics: a String operand is MARKUP, parsed and
|
|
153
|
+
# appended (cleanup code inserts "<bibliography/>" style
|
|
154
|
+
# strings); the adapter path would mint a literal text node.
|
|
155
|
+
# Metanorma's Nokogiri returns the new children as a NodeSet
|
|
156
|
+
# for markup (misc.add_child("<UnitsML/>").first idiom), self
|
|
157
|
+
# for nodes.
|
|
158
|
+
if node.is_a?(String)
|
|
159
|
+
nodes = context.parse_fragment(node).to_a
|
|
160
|
+
nodes.each { |child| attach_child(child) }
|
|
161
|
+
return NodeSet.new(nodes.map(&:native), context)
|
|
162
|
+
end
|
|
163
|
+
attach_child(node)
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def add_previous_sibling(node)
|
|
168
|
+
# String operands insert every fragment root; returns the new
|
|
169
|
+
# nodes as a NodeSet (metanorma idiom: sect.add_next_sibling(
|
|
170
|
+
# "<bibliography/>").first).
|
|
171
|
+
if node.is_a?(String)
|
|
172
|
+
nodes = context.parse_fragment(node).to_a
|
|
173
|
+
nodes.each { |n| attach_previous_sibling(n) }
|
|
174
|
+
return NodeSet.new(nodes.map(&:native), context)
|
|
175
|
+
end
|
|
176
|
+
attach_previous_sibling(node)
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Nokogiri-compatible assignment forms: node.next = / node.previous =.
|
|
181
|
+
# String operands are parsed as XML fragments, like Nokogiri.
|
|
182
|
+
def next=(node)
|
|
183
|
+
add_next_sibling(node)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def previous=(node)
|
|
187
|
+
add_previous_sibling(node)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def add_next_sibling(node)
|
|
191
|
+
if node.is_a?(String)
|
|
192
|
+
nodes = context.parse_fragment(node).to_a
|
|
193
|
+
anchor = self
|
|
194
|
+
nodes.each do |n|
|
|
195
|
+
attach_next_sibling_to(anchor, n)
|
|
196
|
+
anchor = n
|
|
197
|
+
end
|
|
198
|
+
return NodeSet.new(nodes.map(&:native), context)
|
|
199
|
+
end
|
|
200
|
+
attach_next_sibling_to(self, node)
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def attach_child(node)
|
|
205
|
+
context.bump_children_generation
|
|
103
206
|
node = prepare_node(node)
|
|
104
207
|
adapter.add_child(@native, node.native)
|
|
105
208
|
# Refresh native in case adapter changed identity (e.g., LibXML
|
|
@@ -115,21 +218,40 @@ module Moxml
|
|
|
115
218
|
self
|
|
116
219
|
end
|
|
117
220
|
|
|
118
|
-
def
|
|
221
|
+
def attach_previous_sibling(node)
|
|
222
|
+
context.bump_children_generation
|
|
119
223
|
node = prepare_node(node)
|
|
120
224
|
adapter.add_previous_sibling(@native, node.native)
|
|
225
|
+
# Invalidate the parent's memoized children list. The wrapper-side
|
|
226
|
+
# @parent_node link is only set when this node was yielded through a
|
|
227
|
+
# parent-aware NodeSet; nodes obtained via at()/xpath() carry no such
|
|
228
|
+
# link, so resolve the parent wrapper from the adapter instead.
|
|
229
|
+
parent&.invalidate_children_cache!
|
|
121
230
|
invalidate_parent_children_cache!
|
|
122
231
|
self
|
|
123
232
|
end
|
|
124
233
|
|
|
125
|
-
def
|
|
234
|
+
def attach_next_sibling_to(anchor, node)
|
|
235
|
+
context.bump_children_generation
|
|
126
236
|
node = prepare_node(node)
|
|
127
|
-
adapter.add_next_sibling(
|
|
128
|
-
|
|
237
|
+
adapter.add_next_sibling(anchor.native, node.native)
|
|
238
|
+
anchor.parent&.invalidate_children_cache!
|
|
239
|
+
anchor.invalidate_parent_children_cache!
|
|
129
240
|
self
|
|
130
241
|
end
|
|
131
242
|
|
|
132
243
|
def remove
|
|
244
|
+
# Nokogiri parity: unlinking an already-detached node is a
|
|
245
|
+
# no-op returning self. Re-attach idioms like
|
|
246
|
+
# `t << n.replace(x).remove` (standoc term cleanup) detach with
|
|
247
|
+
# #replace and rely on this; leptris would raise "Not found".
|
|
248
|
+
# The wrapper-side @parent_node link decides when present
|
|
249
|
+
# (add_child sets it); the adapter resolution covers
|
|
250
|
+
# link-less wrappers (at()-resolved nodes), whose parents are
|
|
251
|
+
# canonical since #313.
|
|
252
|
+
return self if @parent_node.nil? && parent.nil?
|
|
253
|
+
|
|
254
|
+
context.bump_children_generation
|
|
133
255
|
invalidate_parent_children_cache!
|
|
134
256
|
adapter.remove(@native)
|
|
135
257
|
invalidate_children_cache!
|
|
@@ -143,6 +265,7 @@ module Moxml
|
|
|
143
265
|
def invalidate_namespace_cache!; end
|
|
144
266
|
|
|
145
267
|
def replace(node)
|
|
268
|
+
context.bump_children_generation
|
|
146
269
|
node = prepare_node(node)
|
|
147
270
|
invalidate_parent_children_cache!
|
|
148
271
|
adapter.replace(@native, node.native)
|
|
@@ -171,6 +294,18 @@ module Moxml
|
|
|
171
294
|
result
|
|
172
295
|
end
|
|
173
296
|
|
|
297
|
+
# Nokogiri-compatible: string interpolation of a node serializes
|
|
298
|
+
# it ("#{node}" in cleanup code), not Object#to_s.
|
|
299
|
+
def to_s
|
|
300
|
+
to_xml
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Nokogiri-compatible: to_str yields the text content (their C
|
|
304
|
+
# node_to_str; standoc does x.content = x.to_str on <script>).
|
|
305
|
+
def to_str
|
|
306
|
+
text
|
|
307
|
+
end
|
|
308
|
+
|
|
174
309
|
# Memoized against the adapter's serialize generation — the
|
|
175
310
|
# entity-marker flag flips at parse and entity-reference mint,
|
|
176
311
|
# both adapter-level, and the generation bump is the invalidation
|
|
@@ -215,6 +350,42 @@ module Moxml
|
|
|
215
350
|
context)
|
|
216
351
|
end
|
|
217
352
|
|
|
353
|
+
# Nokogiri-compatible sugar: at/search/at_css/css. Nokogiri's
|
|
354
|
+
# at/search auto-detect XPath vs CSS; the XPath-first fallback
|
|
355
|
+
# covers the dominant caller shape (XPath expression strings)
|
|
356
|
+
# while css/at_css serve stylesheet-style selectors.
|
|
357
|
+
def at(expression, namespaces = {})
|
|
358
|
+
at_css_or_xpath(expression, namespaces)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def search(expression, namespaces = {})
|
|
362
|
+
xpath(expression, namespaces)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def css(_expression)
|
|
366
|
+
raise Moxml::NotImplementedError,
|
|
367
|
+
"CSS selectors are not supported; use search with XPath"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def at_css(_expression)
|
|
371
|
+
raise Moxml::NotImplementedError,
|
|
372
|
+
"CSS selectors are not supported; use at with XPath"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def at_css_or_xpath(expression, namespaces = {})
|
|
376
|
+
at_xpath(expression, namespaces)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Nokogiri-compatible depth-first traversal: yields self, then
|
|
380
|
+
# children recursively (document-order visitor).
|
|
381
|
+
def traverse(&block)
|
|
382
|
+
return to_enum(:traverse) unless block
|
|
383
|
+
|
|
384
|
+
yield self
|
|
385
|
+
children.each { |child| child.traverse(&block) }
|
|
386
|
+
self
|
|
387
|
+
end
|
|
388
|
+
|
|
218
389
|
# Convenience find methods (aliases for xpath methods)
|
|
219
390
|
def find(xpath_expression, namespaces = {})
|
|
220
391
|
at_xpath(xpath_expression, namespaces)
|
|
@@ -307,7 +478,15 @@ module Moxml
|
|
|
307
478
|
# the document node.
|
|
308
479
|
#
|
|
309
480
|
# @return [NodeSet] ancestors ordered nearest-first
|
|
310
|
-
|
|
481
|
+
# Nokogiri-compatible: an element-name argument filters the set.
|
|
482
|
+
def ancestors(selector = nil)
|
|
483
|
+
all = _ancestors_all
|
|
484
|
+
return all unless selector
|
|
485
|
+
|
|
486
|
+
all.select { |a| a.name == selector.to_s }
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def _ancestors_all
|
|
311
490
|
return NodeSet.new([], context) if document?
|
|
312
491
|
|
|
313
492
|
natives = []
|
|
@@ -371,18 +550,9 @@ module Moxml
|
|
|
371
550
|
# elements share the same qualified name, keeping paths minimal.
|
|
372
551
|
#
|
|
373
552
|
# @return [String] XPath expression
|
|
374
|
-
# @raise [Moxml::NotImplementedError] for node types other than
|
|
375
|
-
# element and document
|
|
376
553
|
def path
|
|
377
554
|
return "/" if document?
|
|
378
555
|
|
|
379
|
-
unless element?
|
|
380
|
-
raise Moxml::NotImplementedError.new(
|
|
381
|
-
"path is only supported for element and document nodes",
|
|
382
|
-
feature: "path",
|
|
383
|
-
)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
556
|
segments = []
|
|
387
557
|
current = self
|
|
388
558
|
while current && !current.document?
|
|
@@ -392,6 +562,19 @@ module Moxml
|
|
|
392
562
|
"/#{segments.join('/')}"
|
|
393
563
|
end
|
|
394
564
|
|
|
565
|
+
# The XPath node-test spelling for this non-element node.
|
|
566
|
+
def path_type_test
|
|
567
|
+
if text? || cdata?
|
|
568
|
+
"text()"
|
|
569
|
+
elsif comment?
|
|
570
|
+
"comment()"
|
|
571
|
+
elsif processing_instruction?
|
|
572
|
+
"processing-instruction()"
|
|
573
|
+
else
|
|
574
|
+
name.to_s
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
395
578
|
# Returns the 1-based line number where this node appears in the
|
|
396
579
|
# source XML, or nil when the underlying adapter does not track
|
|
397
580
|
# source positions.
|
|
@@ -496,19 +679,25 @@ module Moxml
|
|
|
496
679
|
|
|
497
680
|
private
|
|
498
681
|
|
|
499
|
-
# XPath segment for
|
|
500
|
-
#
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
682
|
+
# XPath segment for a node: elements use their qualified name,
|
|
683
|
+
# other node kinds their XPath type test (text(), comment(), ...).
|
|
684
|
+
# A positional predicate is emitted only when same-kind siblings
|
|
685
|
+
# make the segment ambiguous.
|
|
686
|
+
def path_segment_for(node)
|
|
687
|
+
name = node.element? ? node.name : node.path_type_test
|
|
688
|
+
parent = node.parent
|
|
504
689
|
return name unless parent
|
|
505
690
|
|
|
506
|
-
|
|
507
|
-
|
|
691
|
+
same_kind = parent.children.select do |child|
|
|
692
|
+
if node.element?
|
|
693
|
+
child.element? && child.name == name
|
|
694
|
+
else
|
|
695
|
+
!child.element? && child.path_type_test == name
|
|
696
|
+
end
|
|
508
697
|
end
|
|
509
|
-
return name if
|
|
698
|
+
return name if same_kind.size == 1
|
|
510
699
|
|
|
511
|
-
"#{name}[#{
|
|
700
|
+
"#{name}[#{same_kind.find_index(node) + 1}]"
|
|
512
701
|
end
|
|
513
702
|
|
|
514
703
|
def prepare_node(node)
|
|
@@ -591,6 +780,13 @@ module Moxml
|
|
|
591
780
|
def self.wrap_with(node, context, adapter)
|
|
592
781
|
return nil if node.nil?
|
|
593
782
|
|
|
783
|
+
# One C node may have several Ruby representations (leptris
|
|
784
|
+
# native/binding duality, moxml#311); every wrap resolves to
|
|
785
|
+
# the canonical one so memo holders share one invalidation
|
|
786
|
+
# chain. A hit-only lookup: a memoized holder implies a prior
|
|
787
|
+
# walk, which is what populates the canonical map.
|
|
788
|
+
node = adapter.canonical_native(node)
|
|
789
|
+
|
|
594
790
|
# Cache-stable binding nodes carry their wrapper directly
|
|
595
791
|
# (#312): the binding's document-owned cache hands the SAME
|
|
596
792
|
# node object for the engine node's whole lifetime, so an
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -6,6 +6,15 @@ module Moxml
|
|
|
6
6
|
|
|
7
7
|
attr_reader :context
|
|
8
8
|
|
|
9
|
+
# Nokogiri-compatible set difference: nodes in this set absent
|
|
10
|
+
# from the other, identity-compared on natives.
|
|
11
|
+
def -(other)
|
|
12
|
+
excluded = {}.compare_by_identity
|
|
13
|
+
other.native_nodes.each { |n| excluded[n] = true }
|
|
14
|
+
remaining = native_nodes.reject { |n| excluded.key?(n) }
|
|
15
|
+
self.class.new(remaining, @context, @parent_node)
|
|
16
|
+
end
|
|
17
|
+
|
|
9
18
|
# nodes: Array of natives, or an adapter's LazyNodeSet — the
|
|
10
19
|
# native set is held unmaterialized until an operation needs an
|
|
11
20
|
# Array (#+, #<<, #delete, Range slices).
|
|
@@ -76,6 +85,23 @@ module Moxml
|
|
|
76
85
|
end
|
|
77
86
|
alias length size
|
|
78
87
|
|
|
88
|
+
# Nokogiri-compatible: serialize the set's members in order.
|
|
89
|
+
def to_xml(options = {})
|
|
90
|
+
map { |n| n.to_xml(options) }.join
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Nokogiri-compatible: string interpolation of a NodeSet
|
|
94
|
+
# serializes it ("#{nodes}" in cleanup code), not Object#to_s.
|
|
95
|
+
def to_s
|
|
96
|
+
to_xml
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Nokogiri-compatible: Array#flatten and multiple-assignment
|
|
100
|
+
# treat a NodeSet as its wrapped-element array via to_ary.
|
|
101
|
+
def to_ary
|
|
102
|
+
to_a
|
|
103
|
+
end
|
|
104
|
+
|
|
79
105
|
def to_a
|
|
80
106
|
i = 0
|
|
81
107
|
wrapped = wrapped_buffer
|
data/lib/moxml/text.rb
CHANGED
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -892,9 +892,21 @@ module Moxml
|
|
|
892
892
|
end
|
|
893
893
|
end
|
|
894
894
|
|
|
895
|
-
# Union: | (pipe)
|
|
896
|
-
|
|
897
|
-
|
|
895
|
+
# Union: | (pipe). Unions are N-ARY — the parser flattens
|
|
896
|
+
# a|b|c into one union node — and may nest (parenthesized
|
|
897
|
+
# groups become union children). Every member's nodes flow
|
|
898
|
+
# through the block when one is given (nested unions), else
|
|
899
|
+
# into one result NodeSet (#315: the old two-children
|
|
900
|
+
# destructure silently dropped every branch after the second).
|
|
901
|
+
def on_pipe(ast, input, &block)
|
|
902
|
+
if block
|
|
903
|
+
code = nil
|
|
904
|
+
ast.children.each do |child|
|
|
905
|
+
fragment = process(child, input, &block)
|
|
906
|
+
code = code ? code.followed_by(fragment) : fragment
|
|
907
|
+
end
|
|
908
|
+
return code
|
|
909
|
+
end
|
|
898
910
|
|
|
899
911
|
union = unique_literal(:union)
|
|
900
912
|
context_var = context_literal
|
|
@@ -903,22 +915,13 @@ module Moxml
|
|
|
903
915
|
nodeset_class = const_ref("Moxml", "NodeSet")
|
|
904
916
|
empty_array = Ruby::Node.new(:array, [])
|
|
905
917
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
# Expressions such as "a | b"
|
|
912
|
-
else
|
|
913
|
-
nodeset_new = Ruby::Node.new(:send,
|
|
914
|
-
[nodeset_class, "new", empty_array,
|
|
915
|
-
context_var])
|
|
916
|
-
|
|
917
|
-
union.assign(nodeset_new)
|
|
918
|
-
.followed_by(process(left, input) { |node| union << node })
|
|
919
|
-
.followed_by(process(right, input) { |node| union << node })
|
|
920
|
-
.followed_by(union)
|
|
918
|
+
code = union.assign(Ruby::Node.new(:send,
|
|
919
|
+
[nodeset_class, "new", empty_array,
|
|
920
|
+
context_var]))
|
|
921
|
+
ast.children.each do |child|
|
|
922
|
+
code = code.followed_by(process(child, input) { |node| union << node })
|
|
921
923
|
end
|
|
924
|
+
code.followed_by(union)
|
|
922
925
|
end
|
|
923
926
|
|
|
924
927
|
# Variable: $variable
|
|
@@ -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
|