moxml 0.5.137 → 0.5.139

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: 43beff2cbabe6c8800099bc8f56a76b4c2510c6158cb632984cd31c8c3893a24
4
- data.tar.gz: e7d0540ce919c428a26b55805f0c78fc25de976c08f2639a83161eab6ffc3b4d
3
+ metadata.gz: 271ff9c6c8d5502c0cd427efdc84e5ba8f123d0008e100a3e7303d478b4a47eb
4
+ data.tar.gz: c87777565ab89568a4195754e4f6c84fe5bd04e230416e0f4e2a5dc605945b54
5
5
  SHA512:
6
- metadata.gz: 8e33f465faabc1da5cc0c7233f70e60592e0080cb4897b81d98eb1b073fedeea21160d2252170e322c8a66b33f4899e1752f96b30555ba0959f4b6f2e46ecd8b
7
- data.tar.gz: 29dde54bd4af8259207a708175309fdf9ad7fd3c4702eacf1e77a92d5fbfbd03b0993d2730c70ddb0c5b30edc41bbc4871e73d10334814fd51b813f2e6905629
6
+ metadata.gz: d5d766ba5edbce1fcba2e14c7227e22c5f3ca0d96a01ab1f2ae2dee5c97f76fc8bb5723e0b1349fe5aee90b026f3532aae07291e17bd2a785761a9f6a7d99e3e
7
+ data.tar.gz: e24616f241e0e846db54db729ad564bf1363f53c7fd7ea422ec6ee3a605138b934d79fe49627b2942c8716b1e2b6655909f2ef84fec0b9ca17fddaa022f23500
@@ -36,7 +36,13 @@ module Moxml
36
36
  # Nokogiri-shaped contract, epilog anchoring included (issue
37
37
  # #130). Built documents reflect their parts immediately
38
38
  # since leptris-ruby 1.9.32 (leptris-ruby#91).
39
- children.concat(doc.children.to_a)
39
+ # leptris-ruby#459 lists the declaration among the binding
40
+ # children; the Nokogiri-shaped facade contract never lists
41
+ # it (the declaration is a preamble part, not a child node)
42
+ # — filtered at the listing seam.
43
+ children.concat(
44
+ doc.children.to_a.grep_v(::Leptris::XML::Declaration),
45
+ )
40
46
 
41
47
  texts = attachments.get(doc, :document_text)
42
48
  children.concat(texts) if texts
@@ -156,6 +162,13 @@ module Moxml
156
162
  next
157
163
  end
158
164
 
165
+ # leptris-ruby#459 exposes the declaration as a
166
+ # Document#children entry; the preamble above owns the
167
+ # emission (a second line here duplicates it — and the
168
+ # 1.9.360 0-arity Declaration#to_xml raises under the
169
+ # options-passing call).
170
+ next if child.is_a?(::Leptris::XML::Declaration)
171
+
159
172
  parts << raw_serialize(child, options) << "\n"
160
173
  end
161
174
 
@@ -92,8 +92,20 @@ module Moxml
92
92
  # NativeNode#[]), so the adapter's native fast paths speak to
93
93
  # the native layer through these bound entries — bind_call is
94
94
  # the same C dispatch without the shadow lookup.
95
+ # Always defined: nothing can be an instance of an anonymous
96
+ # class, so on the FFI-only lane is_a?(NN) is uniformly false
97
+ # and the ungated native-node conditionals keep their
98
+ # semantics without the compiled ext.
99
+ NN = NATIVE_READ_LAYER ? ::Leptris::XML::NativeNode : Class.new
100
+
101
+ # The binding's Document.parse gained the strict: kwarg later
102
+ # than the native layer's default-strict behavior — probe the
103
+ # face instead of version-gating (the RECOVER_MODE pattern).
104
+ STRICT_PARSE_KWARG =
105
+ ::Leptris::XML::Document.method(:parse)
106
+ .parameters.any? { |_, key| key == :strict }
107
+
95
108
  if NATIVE_READ_LAYER
96
- NN = ::Leptris::XML::NativeNode
97
109
  NN_ATTRIBUTE = NN.instance_method(:attribute)
98
110
  NN_NAME = NN.instance_method(:name)
99
111
  NN_PARENT = NN.instance_method(:parent)
@@ -397,6 +409,27 @@ module Moxml
397
409
  end
398
410
  end
399
411
 
412
+ class << self
413
+ # These three live OUTSIDE the NATIVE_READ_LAYER gate:
414
+ # ungated methods (register_mint_klasses!, the
415
+ # create/register seams) and the specs' singleton const_get
416
+ # read them, and on the FFI-only lane the gate's Identity
417
+ # classes do not exist. NATIVE_MUTATIONS_COHERENT is false
418
+ # without the native layer, so all three evaluate false
419
+ # there.
420
+ NATIVE_IDENTITY =
421
+ NATIVE_MUTATIONS_COHERENT &&
422
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.4")
423
+
424
+ NATIVE_C_MINT =
425
+ NATIVE_IDENTITY &&
426
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.255.2")
427
+
428
+ NATIVE_KLASS_CHILDREN =
429
+ NATIVE_IDENTITY &&
430
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.2")
431
+ end
432
+
400
433
  if NATIVE_READ_LAYER
401
434
  # root NativeNode -> binding document (recorded at #root
402
435
  # mint; the native layer exposes no document accessor).
@@ -501,10 +534,6 @@ module Moxml
501
534
  TEXT.include(Reads)
502
535
  end
503
536
 
504
- NATIVE_IDENTITY =
505
- NATIVE_MUTATIONS_COHERENT &&
506
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.4")
507
-
508
537
  # leptris-ruby#246 klass-propagating reads: the read
509
538
  # family (children/element_children/next_sibling/parent)
510
539
  # mints the mapped subclass per kind, so children arrive
@@ -521,14 +550,6 @@ module Moxml
521
550
  # classes — walked and built nodes arrive already carrying the
522
551
  # contract, and the Ruby shell mint drops out entirely
523
552
  # (moxml#288: the entire remaining walk/build gap).
524
- NATIVE_C_MINT =
525
- NATIVE_IDENTITY &&
526
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.255.2")
527
-
528
- NATIVE_KLASS_CHILDREN =
529
- NATIVE_IDENTITY &&
530
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.2")
531
-
532
553
  if NATIVE_KLASS_CHILDREN
533
554
  kind_classes = [
534
555
  Identity::ELEMENT,
@@ -1065,6 +1086,10 @@ module Moxml
1065
1086
  ::Leptris::XML::Document.parse(
1066
1087
  processed,
1067
1088
  readonly: options[:readonly] == true,
1089
+ # The FFI fallback lane parses leniently by default —
1090
+ # the strict intent must ride explicitly or strict
1091
+ # callers silently get a best-effort tree.
1092
+ **(options[:strict] && STRICT_PARSE_KWARG ? { strict: true } : {}),
1068
1093
  options: parse_flags(options, recover: recover,
1069
1094
  context: _context),
1070
1095
  )
@@ -1343,13 +1368,22 @@ module Moxml
1343
1368
  # comment, cdata, pi. Parsed documents register right after
1344
1369
  # the binding mint. Unregistered bindings (older floors, the
1345
1370
  # C-mint off) are unchanged — the shell path stays.
1346
- MINT_KIND_CLASSES = [
1347
- Identity::ELEMENT,
1348
- Identity::TEXT,
1349
- Identity::COMMENT,
1350
- Identity::CDATA,
1351
- Identity::PROCESSING_INSTRUCTION,
1352
- ].freeze
1371
+ # Identity lives under the NATIVE_READ_LAYER gate — on the
1372
+ # FFI-only lane (windows, or a binding whose native bundle
1373
+ # fails to load) the constant must not be referenced at load
1374
+ # time. register_mint_klasses! never reads it there
1375
+ # (NATIVE_C_MINT is false).
1376
+ MINT_KIND_CLASSES = if NATIVE_READ_LAYER
1377
+ [
1378
+ Identity::ELEMENT,
1379
+ Identity::TEXT,
1380
+ Identity::COMMENT,
1381
+ Identity::CDATA,
1382
+ Identity::PROCESSING_INSTRUCTION,
1383
+ ].freeze
1384
+ else
1385
+ [].freeze
1386
+ end
1353
1387
 
1354
1388
  def register_mint_klasses!(binding_doc)
1355
1389
  return binding_doc unless NATIVE_C_MINT && binding_doc
@@ -2349,7 +2383,7 @@ module Moxml
2349
2383
  # upstream (its absorbed_into? checks are not transitive) —
2350
2384
  # those keep the pin + adoption copy. Entity references
2351
2385
  # keep the marker flow below.
2352
- if NATIVE_DOC_ABSORB && !child.is_a?(NN) &&
2386
+ if NATIVE_READ_LAYER && NATIVE_DOC_ABSORB && !child.is_a?(NN) &&
2353
2387
  child.is_a?(::Leptris::XML::Node) &&
2354
2388
  !child.is_a?(CustomizedLeptris::EntityReference)
2355
2389
  child_doc = child.document
@@ -2447,8 +2481,11 @@ module Moxml
2447
2481
  end
2448
2482
  # 1.9.311: an absorbed element source moves through the raw
2449
2483
  # engine faces — the binding's sibling insert drops it.
2450
- if new_node.is_a?(::Leptris::XML::Element) &&
2451
- !new_node.is_a?(::Leptris::XML::NativeNode)
2484
+ # NATIVE_READ_LAYER: the absorb needs the raw engine add to
2485
+ # land the source; on the FFI lane the binding's own insert
2486
+ # handles it and the absorbed-source splice loses the node.
2487
+ if NATIVE_READ_LAYER && new_node.is_a?(::Leptris::XML::Element) &&
2488
+ !new_node.is_a?(NN)
2452
2489
  new_doc = new_node.document
2453
2490
  node_doc = node.document
2454
2491
  if new_doc && node_doc && !new_doc.equal?(node_doc)
@@ -2477,8 +2514,11 @@ module Moxml
2477
2514
  pin_foreign_owner_document(node, new_node)
2478
2515
  # 1.9.311: an absorbed element source moves through the raw
2479
2516
  # engine faces — the binding's sibling insert drops it.
2480
- if new_node.is_a?(::Leptris::XML::Element) &&
2481
- !new_node.is_a?(::Leptris::XML::NativeNode)
2517
+ # NATIVE_READ_LAYER: the absorb needs the raw engine add to
2518
+ # land the source; on the FFI lane the binding's own insert
2519
+ # handles it and the absorbed-source splice loses the node.
2520
+ if NATIVE_READ_LAYER && new_node.is_a?(::Leptris::XML::Element) &&
2521
+ !new_node.is_a?(NN)
2482
2522
  new_doc = new_node.document
2483
2523
  node_doc = node.document
2484
2524
  if new_doc && node_doc && !new_doc.equal?(node_doc)
@@ -244,6 +244,9 @@ module Moxml
244
244
  return NATIVE_NODE_TYPE_MAP[node.node_type] || :unknown
245
245
  end
246
246
  return :document if node.is_a?(::LibXML::XML::Document)
247
+ # Attr is a separate class (not ::Node) — attribute-terminal
248
+ # XPath results mint as plain Node without this.
249
+ return :attribute if node.is_a?(::LibXML::XML::Attr)
247
250
 
248
251
  wrapper_type = WRAPPER_NODE_TYPE_MAP[node.class]
249
252
  return wrapper_type if wrapper_type
@@ -949,6 +952,16 @@ module Moxml
949
952
  native_node = unpatch_node(node)
950
953
  return [] unless native_node
951
954
 
955
+ # libxml-ruby's Document#find returns EMPTY for relative
956
+ # paths (nokogiri/libxml2 evaluate them fine): a relative
957
+ # expression from the document node is equivalent to the
958
+ # absolute form — the document's only name-testable child
959
+ # is the root — so rewrite instead of losing the match.
960
+ if native_node.is_a?(::LibXML::XML::Document) &&
961
+ expression.match?(/\A[A-Za-z_*@]/)
962
+ expression = "/#{expression}"
963
+ end
964
+
952
965
  # Build namespace context for LibXML
953
966
  # LibXML requires ALL prefixes in the XPath to be registered
954
967
  ns_context = build_xpath_namespaces(native_node, namespaces)
@@ -626,7 +626,11 @@ module Moxml
626
626
  end
627
627
 
628
628
  def xpath(node, expression, namespaces = nil)
629
- result = node.xpath(expression, namespaces)
629
+ # An explicit bindings hash — even EMPTY — makes nokogiri
630
+ # register a prefix context, which disables libxml2's
631
+ # literal xmlns: default-namespace handling. Only pass the
632
+ # hash when the caller actually bound something.
633
+ result = namespaces.nil? || namespaces.empty? ? node.xpath(expression) : node.xpath(expression, namespaces)
630
634
  # Adapter contract: Array<native> | LazyNodeSet | scalar
631
635
  # (count(), string-length(), boolean functions return
632
636
  # scalars). The native node-set passes through lazily —
@@ -604,10 +604,13 @@ module Moxml
604
604
  end
605
605
 
606
606
  def xpath(node, expression, namespaces = {})
607
+ # XPath.match for every expression: get_elements only
608
+ # handles simple child-name paths — attribute terminals,
609
+ # absolute paths and predicates silently degrade there.
607
610
  if namespaces && !namespaces.empty?
608
- ::REXML::XPath.match(node, expression, namespaces)
611
+ ::REXML::XPath.match(node, expression, namespaces).to_a
609
612
  else
610
- node.get_elements(expression).to_a
613
+ ::REXML::XPath.match(node, expression).to_a
611
614
  end
612
615
  rescue ::REXML::ParseException => e
613
616
  raise Moxml::XPathError.new(
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.137"
4
+ VERSION = "0.5.139"
5
5
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Cross-adapter XPath idioms that nokogiri consumers rely on:
6
+ #
7
+ # - The literal xmlns: prefix resolves against the default namespace
8
+ # (the libxml2 quirk that is nokogiri's documented way to match
9
+ # default-namespaced elements; leptris carries it since engine
10
+ # 1.9.359 / leptris-ruby#469; moxml's own engine always has).
11
+ # - Attribute-terminal steps work in every path position
12
+ # (leptris-ruby#458's table: //a/@id, //@id, r/a/@id,
13
+ # //*[f()]/@id — fixed by the engine's VM path work).
14
+ RSpec.describe "XPath idioms across adapters" do
15
+ let(:default_ns_xml) do
16
+ '<svgmap xmlns="http://www.example.org/doc"><target href="a"/></svgmap>'
17
+ end
18
+ let(:plain_xml) { '<r><a id="x"><b/></a></r>' }
19
+
20
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
21
+ next if adapter_name == :headed_ox # same engine as :ox
22
+
23
+ context "with #{adapter_name}" do
24
+ let(:ctx) { Moxml.new(adapter_name) }
25
+
26
+ before do
27
+ if adapter_name == :leptris &&
28
+ Gem::Version.new(Leptris::VERSION) < Gem::Version.new("1.9.359.0")
29
+ skip "binding #{Leptris::VERSION} predates the xmlns: fix (1.9.359)"
30
+ end
31
+ if adapter_name == :oga
32
+ # oga's own Ruby XPath engine has no xmlns: default-ns
33
+ # support (third-party; explicit bindings work).
34
+ skip "oga's XPath engine does not implement the xmlns: idiom"
35
+ end
36
+ end
37
+
38
+ it "matches default-namespaced elements via the xmlns: prefix" do
39
+ doc = ctx.parse(default_ns_xml)
40
+ targets = doc.root.xpath("./xmlns:target")
41
+ expect(targets.length).to eq(1)
42
+ expect(targets.first.name).to eq("target")
43
+ end
44
+
45
+ it "keeps explicit namespace bindings winning over the idiom" do
46
+ doc = ctx.parse(default_ns_xml)
47
+ targets = doc.root.xpath("./xmlns:target", "xmlns" => "urn:bogus")
48
+ expect(targets).to be_empty
49
+ end
50
+
51
+ it "resolves attribute-terminal steps in every path position" do
52
+ doc = ctx.parse(plain_xml)
53
+ expect(doc.xpath("//a/@id").first&.value).to eq("x")
54
+ expect(doc.xpath("//@id").first&.value).to eq("x")
55
+ expect(doc.xpath("r/a/@id").first&.value).to eq("x")
56
+ expect(
57
+ doc.xpath("//*[local-name()='a']/@id").first&.value,
58
+ ).to eq("x")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # leptris-ruby#459 exposes the declaration as a Document#children
6
+ # entry (binding 1.9.360). The serialize child walk must skip it —
7
+ # the composed preamble owns the emission — or declaration-bearing
8
+ # round trips duplicate the line (1.9.359) or raise (1.9.360's
9
+ # 0-arity Declaration#to_xml under the options-passing call).
10
+ RSpec.describe "leptris document declaration child" do
11
+ let(:ctx) { Moxml.new(:leptris) }
12
+ let(:xml) { %(<?xml version="1.0" encoding="UTF-8"?>\n<documents></documents>) }
13
+
14
+ before do
15
+ unless Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.360.0")
16
+ skip "binding #{Leptris::VERSION} predates the declaration child (1.9.360)"
17
+ end
18
+ end
19
+
20
+ it "round trips byte-identical with exactly one declaration" do
21
+ doc = ctx.parse(xml)
22
+ out = doc.to_xml
23
+
24
+ expect(out).to eq("#{xml}\n")
25
+ expect(out.scan("<?xml").length).to eq(1)
26
+ end
27
+
28
+ it "never lists the declaration among document children" do
29
+ doc = ctx.parse(xml)
30
+ first = doc.children.to_a
31
+ second = doc.children.to_a
32
+
33
+ expect(first.map(&:node_type)).to eq(%i[element])
34
+ expect(second.map(&:node_type)).to eq(first.map(&:node_type))
35
+ expect(doc.to_xml).to eq("#{xml}\n")
36
+ end
37
+
38
+ it "keeps declaration-less documents unchanged" do
39
+ doc = ctx.parse("<r/>")
40
+ expect(doc.to_xml).to eq("<r></r>\n")
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.137
4
+ version: 0.5.139
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-15 00:00:00.000000000 Z
11
+ date: 2026-10-16 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
@@ -313,6 +313,7 @@ files:
313
313
  - spec/consistency/eol_normalization_spec.rb
314
314
  - spec/consistency/html_doctype_spec.rb
315
315
  - spec/consistency/round_trip_spec.rb
316
+ - spec/consistency/xpath_idioms_spec.rb
316
317
  - spec/consistency/xslt_spec.rb
317
318
  - spec/examples/README.md
318
319
  - spec/examples/attribute_examples_spec.rb
@@ -452,6 +453,7 @@ files:
452
453
  - spec/moxml/adapter/leptris_attribute_snapshot_spec.rb
453
454
  - spec/moxml/adapter/leptris_binding_cap_spec.rb
454
455
  - spec/moxml/adapter/leptris_doc_parts_spec.rb
456
+ - spec/moxml/adapter/leptris_document_declaration_spec.rb
455
457
  - spec/moxml/adapter/leptris_parse_skips_spec.rb
456
458
  - spec/moxml/adapter/leptris_record_driver_spec.rb
457
459
  - spec/moxml/adapter/leptris_sax_dup_attr_spec.rb