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.
@@ -0,0 +1,386 @@
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
+
86
+ describe "Node#<< append sugar" do
87
+ it "appends parsed fragments from strings" do
88
+ doc = Moxml.parse("<r/>")
89
+ doc.root << "<a/><b/>"
90
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
91
+ end
92
+
93
+ it "appends plain text as a text node" do
94
+ text_parent = Moxml.parse("<p/>").root
95
+ text_parent << "hello"
96
+ expect(text_parent.text).to eq("hello")
97
+ end
98
+
99
+ it "appends nodes" do
100
+ doc = Moxml.parse("<r/>")
101
+ doc.root << Moxml.parse("<b/>").root
102
+ expect(doc.root.children.map(&:name)).to eq(%w[b])
103
+ end
104
+
105
+ it "returns self for chaining" do
106
+ doc = Moxml.parse("<r/>")
107
+ expect(doc.root << "<a/>").to be(doc.root)
108
+ end
109
+ end
110
+
111
+ describe "Element#delete" do
112
+ it "removes an attribute by name" do
113
+ doc = Moxml.parse("<r annex='yes' id='x'/>")
114
+ doc.root.delete("annex")
115
+ expect(doc.root["annex"]).to be_nil
116
+ expect(doc.root["id"]).to eq("x")
117
+ end
118
+ end
119
+
120
+ describe "Element#children=" do
121
+ it "replaces all children with a string" do
122
+ doc = Moxml.parse("<r><old/>text</r>")
123
+ doc.root.children = "<a/><b/>"
124
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
125
+ end
126
+
127
+ it "replaces with a node" do
128
+ doc = Moxml.parse("<r><old/></r>")
129
+ node = Moxml.parse("<x/>").root
130
+ doc.root.children = node
131
+ expect(doc.root.children.map(&:name)).to eq(%w[x])
132
+ end
133
+
134
+ it "replaces with an array of nodes" do
135
+ doc = Moxml.parse("<r><old/></r>")
136
+ frag = Moxml.parse("<w><x/><y/></w>").root.children
137
+ doc.root.children = frag
138
+ expect(doc.root.children.map(&:name)).to eq(%w[x y])
139
+ end
140
+
141
+ it "invalidates memoized children holders" do
142
+ doc = Moxml.parse("<r><old/></r>")
143
+ root = doc.root
144
+ root.children
145
+ root.children = "<new/>"
146
+ expect(root.children.map(&:name)).to eq(%w[new])
147
+ end
148
+ end
149
+
150
+ describe "Element#elements" do
151
+ it "returns only element children" do
152
+ doc = Moxml.parse("<r>text<a/><!-- c --><b/></r>")
153
+ expect(doc.root.elements.map(&:name)).to eq(%w[a b])
154
+ expect(doc.root.element_children.map(&:name)).to eq(%w[a b])
155
+ end
156
+
157
+ it "stays fresh across mutations" do
158
+ doc = Moxml.parse("<r><a/></r>")
159
+ root = doc.root
160
+ expect(root.elements.map(&:name)).to eq(%w[a])
161
+ root.add_child(doc.create_element("b"))
162
+ expect(root.elements.map(&:name)).to eq(%w[a b])
163
+ end
164
+ end
165
+
166
+ describe "NodeSet#- difference" do
167
+ it "removes nodes present in the other set" do
168
+ doc = Moxml.parse("<r><a/><b/><c/></r>")
169
+ diff = doc.root.children - doc.search("//a")
170
+ expect(diff.map(&:name).sort).to eq(%w[b c])
171
+ end
172
+ end
173
+
174
+ describe "NodeSet#to_ary" do
175
+ it "flattens inside arrays" do
176
+ doc = Moxml.parse("<r><a/><b/></r>")
177
+ flat = [doc.root, doc.root.elements].flatten
178
+ expect(flat.map(&:name)).to include("r", "a", "b")
179
+ end
180
+ end
181
+
182
+ describe "Node#ancestors(selector)" do
183
+ it "filters ancestors by element name" do
184
+ doc = Moxml.parse("<r><table><tr><td><p/></td></tr></table></r>")
185
+ p_node = doc.at("//p")
186
+ expect(p_node.ancestors("table").map(&:name)).to eq(%w[table])
187
+ expect(p_node.ancestors.map(&:name)).to include("td", "tr", "table")
188
+ end
189
+ end
190
+
191
+ describe "Node#next_element/previous_element" do
192
+ it "skips interleaved text nodes" do
193
+ doc = Moxml.parse("<r><a/>text<b/></r>")
194
+ a = doc.at("//a")
195
+ expect(a.next_element.name).to eq("b")
196
+ expect(doc.at("//b").previous_element.name).to eq("a")
197
+ end
198
+ end
199
+
200
+ describe "Node#parent= reparenting" do
201
+ it "moves a node under a new parent" do
202
+ doc = Moxml.parse("<r><from><p/></from><to/></r>")
203
+ p_node = doc.at("//p")
204
+ p_node.parent = doc.at("//to")
205
+ expect(doc.at("//to/p")).not_to be_nil
206
+ expect(doc.at("//from/p")).to be_nil
207
+ end
208
+ end
209
+
210
+ describe "NodeSet#to_xml" do
211
+ it "serializes the set's members in order" do
212
+ doc = Moxml.parse("<r><a>x</a>tail</r>")
213
+ expect(doc.root.children.to_xml).to eq("<a>x</a>tail")
214
+ end
215
+ end
216
+
217
+ describe "Node#add_first_child" do
218
+ it "inserts before existing children and handles empty parents" do
219
+ doc = Moxml.parse("<r><b/></r>")
220
+ doc.root.add_first_child("<a/>")
221
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
222
+ Moxml.parse("<r/>").root.add_first_child("<x/>")
223
+ end
224
+ end
225
+
226
+ describe "Node#remove on a detached node" do
227
+ it "is a no-op returning self (Nokogiri parity)" do
228
+ doc = Moxml.parse("<r><p><c/></p><t/></r>")
229
+ p_node = doc.at("//p")
230
+ t = doc.create_element("d")
231
+ p_node.replace(t)
232
+ expect(p_node.parent).to be_nil
233
+ expect(p_node.remove).to equal(p_node)
234
+ t << p_node
235
+ expect(t.children.map(&:name)).to eq(%w[p])
236
+ expect(doc.at("//d/p/c")).not_to be_nil
237
+ end
238
+ end
239
+
240
+ describe "Element#replace + re-attach under the replacement" do
241
+ it "does not cycle the tree (leptris stale-run splice)" do
242
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
243
+ a = doc.at("//a")
244
+ t = doc.create_element("t")
245
+ a.replace(t)
246
+ t << a
247
+ expect(doc.root.children.map(&:name)).to eq(%w[t b])
248
+ expect(t.children.map(&:name)).to eq(%w[a])
249
+ expect(doc.at("//t/a/text()").text).to eq("x")
250
+ end
251
+ end
252
+
253
+ describe "Node/NodeSet#to_s interpolation" do
254
+ it "serializes instead of Object#to_s" do
255
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
256
+ # rubocop:disable Style/RedundantInterpolation -- the interpolation IS the contract under test
257
+ node = doc.at("//a")
258
+ expect("#{node}").to eq("<a>x</a>")
259
+ children = doc.root.children
260
+ expect("#{children}").to eq("<a>x</a><b></b>")
261
+ # rubocop:enable Style/RedundantInterpolation
262
+ end
263
+ end
264
+
265
+ describe "Element#default_namespace=" do
266
+ it "binds the element to the default namespace" do
267
+ doc = Moxml.parse("<r><math><mi>x</mi></math></r>")
268
+ math = doc.at("//math")
269
+ math.default_namespace = "http://www.w3.org/1998/Math/MathML"
270
+ expect(math.namespace_uri.to_s).to eq("http://www.w3.org/1998/Math/MathML")
271
+ expect(math.to_xml).to include('xmlns="http://www.w3.org/1998/Math/MathML"')
272
+ end
273
+ end
274
+
275
+ describe "Node#to_str" do
276
+ it "yields the text content" do
277
+ doc = Moxml.parse("<r><s>a<b>c</b>d</s></r>")
278
+ expect(doc.at("//s").to_str).to eq("acd")
279
+ expect(doc.at("//b").to_str).to eq("c")
280
+ end
281
+ end
282
+
283
+ describe "Element#content=" do
284
+ it "replaces children with the given text" do
285
+ doc = Moxml.parse("<r><s><b>x</b>y</s></r>")
286
+ s = doc.at("//s")
287
+ s.content = "1 < 2"
288
+ expect(s.children.size).to eq(1)
289
+ expect(s.content).to eq("1 < 2")
290
+ expect(s.to_xml).to eq("<s>1 &lt; 2</s>")
291
+ end
292
+ end
293
+
294
+ describe "Node#next/previous readers" do
295
+ it "return adjacent siblings" do
296
+ doc = Moxml.parse("<r><a/>mid<b/></r>")
297
+ a = doc.at("//a")
298
+ expect(a.next.name).to eq("text")
299
+ expect(a.next.next.name).to eq("b")
300
+ expect(doc.at("//b").previous.name).to eq("text")
301
+ expect(doc.at("//b").previous.previous.name).to eq("a")
302
+ end
303
+ end
304
+
305
+ describe "Node#path for non-element nodes" do
306
+ it "builds type-test segments" do
307
+ doc = Moxml.parse("<r><p>a<!--c--></p><p>b</p></r>")
308
+ expect(doc.at("//p").path).to eq("/r/p[1]")
309
+ texts = doc.xpath("//p/text()")
310
+ expect(texts[0].path).to eq("/r/p[1]/text()")
311
+ expect(doc.xpath("//p/comment()").first.path).to eq("/r/p[1]/comment()")
312
+ end
313
+ end
314
+
315
+ describe "Document#root= with a foreign root" do
316
+ it "adopts the subtree from another document" do
317
+ doc = Moxml.parse("<old><x/></old>")
318
+ other = Moxml.parse(%(<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>))
319
+ doc.root = other.root
320
+ expect(doc.to_xml(indent: 0).strip)
321
+ .to eq(%(<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>))
322
+ expect(doc.root.namespace_uri.to_s).to eq("urn:x")
323
+ end
324
+ end
325
+
326
+ describe "String operands are markup, not literal text" do
327
+ it "add_child / add_next_sibling / add_previous_sibling parse strings" do
328
+ doc = Moxml.parse("<r><a/></r>")
329
+ a = doc.at("//a")
330
+ a.add_child("<c/>")
331
+ a.add_next_sibling("<d/>")
332
+ a.add_previous_sibling("<b/>")
333
+ expect(a.children.map(&:name)).to eq(%w[c])
334
+ expect(doc.root.children.map(&:name)).to eq(%w[b a d])
335
+ expect(doc.at("//r/a/c")).not_to be_nil
336
+ end
337
+ end
338
+
339
+ describe "Markup-string insertion returns a NodeSet of new nodes" do
340
+ it "add_child / add_next_sibling return the new nodes; nodes return self" do
341
+ doc = Moxml.parse("<r><a/></r>")
342
+ a = doc.at("//a")
343
+ kids = a.add_child("<c1/><c2/>")
344
+ expect(kids).to be_a(Moxml::NodeSet)
345
+ expect(kids.map(&:name)).to eq(%w[c1 c2])
346
+ sibs = a.add_next_sibling("<n1/><n2/>")
347
+ expect(sibs.map(&:name)).to eq(%w[n1 n2])
348
+ expect(doc.root.children.map(&:name)).to eq(%w[a n1 n2])
349
+ prev = a.add_previous_sibling("<p1/><p2/>")
350
+ expect(prev.map(&:name)).to eq(%w[p1 p2])
351
+ expect(doc.root.children.map(&:name)).to eq(%w[p1 p2 a n1 n2])
352
+ expect(a.add_child(doc.at("//p1"))).to equal(a)
353
+ end
354
+ end
355
+
356
+ describe "default_namespace= idempotence" do
357
+ it "does not duplicate an identical in-scope default namespace" do
358
+ doc = Moxml.parse(%(<r xmlns="urn:x"><math xmlns="urn:m"/></r>))
359
+ math = doc.at("//m:math", "m" => "urn:m")
360
+ math.default_namespace = "urn:m"
361
+ expect(math.to_xml).to eq(%(<math xmlns="urn:m"></math>))
362
+ end
363
+ end
364
+
365
+ describe "xpath with namespace bindings" do
366
+ it "prefixed star and name tests honor the namespace URI" do
367
+ # m must be DECLARED on the element — a bare m:title with only
368
+ # a default xmlns is a no-namespace element whose literal name
369
+ # carries the colon, and no prefix binding can select it.
370
+ doc = Moxml.parse(%(<r><bibdata/><m:title xmlns:m="urn:x"/></r>))
371
+ hits = doc.root.xpath(".//m:*", "m" => "urn:unitsml")
372
+ expect(hits.map(&:name)).to eq([])
373
+ named = doc.root.at_xpath(".//m:title", "m" => "urn:x")
374
+ expect(named&.name).to eq("title")
375
+ none = doc.root.at_xpath(".//m:title", "m" => "urn:unitsml")
376
+ expect(none).to be_nil
377
+ end
378
+ end
379
+
380
+ describe "Element#inner_xml" do
381
+ it "serializes the children" do
382
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
383
+ expect(doc.root.inner_xml).to eq("<a>x</a><b/>")
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # moxml#315: unions are N-ARY in the parser (a|b|c parses to one
6
+ # union node); the compiler's two-children destructure silently
7
+ # dropped every branch after the second. The reserved xmlns prefix
8
+ # hid the drop for namespaced consumers because those queries rode
9
+ # the native seam; when they fell back to the Ruby engine (or on
10
+ # adapters whose engine IS this one, e.g. ox), branches vanished.
11
+ RSpec.describe "XPath union compilation" do
12
+ let(:ctx) { Moxml.new(:leptris) }
13
+ let(:doc) { ctx.parse('<r xmlns="urn:x"><a>1</a><b>2</b><c>3</c></r>') }
14
+
15
+ it "keeps every branch of a flat n-ary union (moxml#315)" do
16
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
17
+ names = r.xpath("./xmlns:a|./xmlns:b|./xmlns:c", { "xmlns" => "urn:x" })
18
+ .map(&:text)
19
+ expect(names).to eq(%w[1 2 3])
20
+ end
21
+
22
+ it "keeps branches under the reserved xmlns prefix through the engine" do
23
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
24
+ ast = Moxml::XPath::Parser.parse("./xmlns:a|./xmlns:b|./xmlns:c")
25
+ proc_c = Moxml::XPath::Compiler.compile_with_cache(ast, namespaces: { "xmlns" => "urn:x" })
26
+ expect(proc_c.call(r).to_a.map(&:text)).to eq(%w[1 2 3])
27
+ end
28
+
29
+ it "keeps parenthesized nested unions" do
30
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
31
+ ast = Moxml::XPath::Parser.parse("(./xmlns:a|./xmlns:b)|./xmlns:c")
32
+ proc_c = Moxml::XPath::Compiler.compile_with_cache(ast, namespaces: { "xmlns" => "urn:x" })
33
+ expect(proc_c.call(r).to_a.map(&:text)).to eq(%w[1 2 3])
34
+ end
35
+
36
+ it "unions still work on adapters whose engine is the Ruby engine" do
37
+ ctx_ox = Moxml.new(:ox)
38
+ doc_ox = ctx_ox.parse("<r><a>1</a><b>2</b><c>3</c></r>")
39
+ r = doc_ox.root
40
+ expect(r.xpath("./a|./b|./c").map(&:text)).to eq(%w[1 2 3])
41
+ end
42
+ 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.100
4
+ version: 0.5.102
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-01 00:00:00.000000000 Z
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
@@ -557,6 +560,7 @@ files:
557
560
  - spec/moxml/xpath/parser_spec.rb
558
561
  - spec/moxml/xpath/ruby/generator_spec.rb
559
562
  - spec/moxml/xpath/ruby/node_spec.rb
563
+ - spec/moxml/xpath/union_spec.rb
560
564
  - spec/moxml/xpath_capabilities_spec.rb
561
565
  - spec/moxml/xpath_spec.rb
562
566
  - spec/moxml/xslt/stylesheet_spec.rb