moxml 0.5.33 → 0.5.34
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/leptris.rb +31 -6
- data/lib/moxml/adapter/nokogiri.rb +8 -0
- data/lib/moxml/adapter/oga.rb +9 -0
- data/lib/moxml/adapter/ox.rb +15 -8
- data/lib/moxml/adapter/rexml.rb +4 -1
- data/lib/moxml/attribute.rb +8 -0
- data/lib/moxml/attribute_resolver.rb +6 -2
- data/lib/moxml/context.rb +17 -6
- data/lib/moxml/element.rb +4 -1
- data/lib/moxml/version.rb +1 -1
- data/spec/integration/contract_parity_spec.rb +13 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d43dcd89303022c12889585b26d38ab3bf199046d869b6412d45c6f3f9249347
|
|
4
|
+
data.tar.gz: e09d8585d90c5a2765040aaf4060bedaececca5228fe7808c11841088415d2e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff23fcc8eaa05cefd83e236a387b707803d8fd06968e9bc84aefd450ff3f2eeca6445fe5a16e3ddd65bcf315b78e140bf27de3f45c30125a34eb6723cee26768
|
|
7
|
+
data.tar.gz: af7d2a75a27d30be26dc4daf750a77661235cf9c203b7e5c0638fde4fe58bed062499f4fa0b1f6648cb6aabc5b45768dfcfa101f095e1037143be67366ad8d94
|
|
@@ -456,8 +456,25 @@ module Moxml
|
|
|
456
456
|
# never applies to a prefixed name.
|
|
457
457
|
element.name = element.name.split(":", 2)[-1] if element.name.include?(":")
|
|
458
458
|
else
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
# Name is the local part re-prefixed (a qname create leaves
|
|
460
|
+
# "p:c"; re-joining without the split would double —
|
|
461
|
+
# issue #208). Declare only when the prefix is not already
|
|
462
|
+
# in scope: under a parent that binds p, a bare name set
|
|
463
|
+
# is enough (leptris resolves through ancestors). Detached
|
|
464
|
+
# elements still get the declaration so a standalone
|
|
465
|
+
# serialize stays well-formed.
|
|
466
|
+
local = element.name.split(":", 2)[-1]
|
|
467
|
+
element.name = "#{prefix}:#{local}"
|
|
468
|
+
already = resolve_prefix_ns(element, prefix)
|
|
469
|
+
needs_decl = already.nil? || already.href.to_s != uri.to_s
|
|
470
|
+
# Detached elements that will be attached under a declaring
|
|
471
|
+
# parent must not carry their own declaration — that is
|
|
472
|
+
# the create_element(qname) + namespace= + attach order
|
|
473
|
+
# (issue #208). Standalone serialize of a still-detached
|
|
474
|
+
# namespaced element is the add_namespace caller's job.
|
|
475
|
+
if needs_decl && !element.parent.nil?
|
|
476
|
+
element.add_namespace_definition(prefix, uri.to_s)
|
|
477
|
+
end
|
|
461
478
|
end
|
|
462
479
|
element
|
|
463
480
|
end
|
|
@@ -626,18 +643,26 @@ module Moxml
|
|
|
626
643
|
end
|
|
627
644
|
|
|
628
645
|
def parent(node)
|
|
646
|
+
# Frequency-ordered: elements dominate navigation and paid
|
|
647
|
+
# three failed compares to reach the else arm.
|
|
629
648
|
case node
|
|
649
|
+
when ::Leptris::XML::Element then root_parent(node)
|
|
630
650
|
when ::Leptris::XML::Document then nil
|
|
631
651
|
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
632
652
|
CustomizedLeptris::DocumentPI then node.parent_doc
|
|
633
653
|
when CustomizedLeptris::TextSegment, CustomizedLeptris::EntityReference then node.parent
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
node.parent || (node.document&.root == node ? node.document : nil)
|
|
654
|
+
# Same body as the Element arm by design (frequency
|
|
655
|
+
# ordering); the generic kinds are cold.
|
|
656
|
+
else root_parent(node) # rubocop:disable Lint/DuplicateBranch
|
|
638
657
|
end
|
|
639
658
|
end
|
|
640
659
|
|
|
660
|
+
# The binding reports the root element as parentless; the
|
|
661
|
+
# moxml contract roots at the document.
|
|
662
|
+
def root_parent(node)
|
|
663
|
+
node.parent || (node.document&.root == node ? node.document : nil)
|
|
664
|
+
end
|
|
665
|
+
|
|
641
666
|
def next_sibling(node)
|
|
642
667
|
node.next_sibling if node.is_a?(::Leptris::XML::Node)
|
|
643
668
|
end
|
|
@@ -200,6 +200,14 @@ module Moxml
|
|
|
200
200
|
end
|
|
201
201
|
|
|
202
202
|
def set_namespace(element, ns)
|
|
203
|
+
# Strip any existing name prefix before binding: create_element
|
|
204
|
+
# with a qname ("p:c") leaves name="p:c", and Nokogiri's
|
|
205
|
+
# bare namespace= then serializes p: + p:c → p:p:c (issue
|
|
206
|
+
# #208). Local-name-only + namespace= is the correct shape;
|
|
207
|
+
# consumers that pass the qname get the same result.
|
|
208
|
+
if ns && element.respond_to?(:name=) && element.name.to_s.include?(":")
|
|
209
|
+
element.name = element.name.split(":", 2)[-1]
|
|
210
|
+
end
|
|
203
211
|
element.namespace = ns
|
|
204
212
|
element
|
|
205
213
|
end
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -172,10 +172,19 @@ module Moxml
|
|
|
172
172
|
def set_namespace(element, ns_or_string)
|
|
173
173
|
if ns_or_string.nil?
|
|
174
174
|
element.namespace_name = nil
|
|
175
|
+
# Drop any name prefix so the element is truly unqualified
|
|
176
|
+
if element.respond_to?(:name=) && element.name.to_s.include?(":")
|
|
177
|
+
element.name = element.name.split(":", 2)[-1]
|
|
178
|
+
end
|
|
175
179
|
set_attribute(element, "xmlns", "")
|
|
176
180
|
return element
|
|
177
181
|
end
|
|
178
182
|
|
|
183
|
+
# Local part only before binding — qname create + namespace=
|
|
184
|
+
# would otherwise serialize p:p:c (issue #208).
|
|
185
|
+
if element.respond_to?(:name=) && element.name.to_s.include?(":")
|
|
186
|
+
element.name = element.name.split(":", 2)[-1]
|
|
187
|
+
end
|
|
179
188
|
element.namespace_name = ns_or_string.to_s
|
|
180
189
|
element
|
|
181
190
|
end
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -167,13 +167,16 @@ module Moxml
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
prefix = ns.prefix
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
# Local part only — a qname create_element leaves
|
|
171
|
+
# "p:c" and re-joining would double the prefix (#208).
|
|
172
|
+
local = element.name.to_s.split(":", 2)[-1]
|
|
173
|
+
element.name = prefix.nil? || prefix.empty? ? local : "#{prefix}:#{local}"
|
|
174
|
+
# Declare only when the element is already in a tree and the
|
|
175
|
+
# prefix is not already bound — detached create+namespace=
|
|
176
|
+
# + attach under a declaring parent must not re-emit xmlns:p.
|
|
177
|
+
if element.is_a?(::Ox::Element) && element.parent
|
|
178
|
+
set_attribute(element, ns.expanded_prefix, ns.uri)
|
|
174
179
|
end
|
|
175
|
-
element.name = [prefix,
|
|
176
|
-
element.name.delete_prefix("xmlns:")].compact.join(":")
|
|
177
180
|
element
|
|
178
181
|
end
|
|
179
182
|
|
|
@@ -300,9 +303,13 @@ module Moxml
|
|
|
300
303
|
|
|
301
304
|
result = node.nodes || []
|
|
302
305
|
# Ox doesn't set parent references during parsing.
|
|
303
|
-
# Set them here so parent/sibling navigation works.
|
|
306
|
+
# Set them here so parent/sibling navigation works. The
|
|
307
|
+
# != node guard skips the write for already-linked (or
|
|
308
|
+
# stale-parented) children — steady-state access pays one
|
|
309
|
+
# comparison instead of a mutation per child.
|
|
304
310
|
result.each do |child|
|
|
305
|
-
child.parent = node if child.is_a?(::Ox::Element)
|
|
311
|
+
child.parent = node if child.is_a?(::Ox::Element) &&
|
|
312
|
+
child.parent != node
|
|
306
313
|
end
|
|
307
314
|
result
|
|
308
315
|
end
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -514,7 +514,10 @@ module Moxml
|
|
|
514
514
|
# Renames the node in place (works for elements and
|
|
515
515
|
# attributes alike); the caller keeps tracking the same
|
|
516
516
|
# native.
|
|
517
|
-
|
|
517
|
+
# Local part only — a qname create_element leaves "p:c"
|
|
518
|
+
# and prefixing it again would double (issue #208).
|
|
519
|
+
local = element.name.to_s.split(":", 2)[-1]
|
|
520
|
+
element.name = "#{prefix}:#{local}"
|
|
518
521
|
element
|
|
519
522
|
end
|
|
520
523
|
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -77,6 +77,14 @@ module Moxml
|
|
|
77
77
|
native_elem && Moxml::Node.wrap(native_elem, context)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# The wrapper's parent tracking is authoritative for attributes
|
|
81
|
+
# (set at materialization by Element#attributes): the generic
|
|
82
|
+
# adapter read raises on engines whose Attr natives expose no
|
|
83
|
+
# #parent (leptris), and the tracked value needs no wrap.
|
|
84
|
+
def parent
|
|
85
|
+
@parent_node
|
|
86
|
+
end
|
|
87
|
+
|
|
80
88
|
def remove
|
|
81
89
|
# The name must be read before the removal — engines free the
|
|
82
90
|
# attribute native, and post-removal reads are use-after-free.
|
|
@@ -106,7 +106,11 @@ module Moxml
|
|
|
106
106
|
def assign(element, name, value)
|
|
107
107
|
name = name.to_s
|
|
108
108
|
adapter = element.adapter
|
|
109
|
-
|
|
109
|
+
# One probe covers both spellings ("xmlns" and "xmlns:*"); a
|
|
110
|
+
# plain attribute literally starting with "xmlns" would only
|
|
111
|
+
# over-invalidate (global bump instead of local) — a no-op
|
|
112
|
+
# for correctness.
|
|
113
|
+
if name.start_with?("xmlns")
|
|
110
114
|
adapter.set_attribute(element.native, name, value)
|
|
111
115
|
element.invalidate_attribute_cache!
|
|
112
116
|
return value
|
|
@@ -144,7 +148,7 @@ module Moxml
|
|
|
144
148
|
def remove(element, name)
|
|
145
149
|
name = name.to_s
|
|
146
150
|
adapter = element.adapter
|
|
147
|
-
if name
|
|
151
|
+
if name.start_with?("xmlns")
|
|
148
152
|
adapter.remove_attribute(element.native, name)
|
|
149
153
|
element.invalidate_attribute_cache!
|
|
150
154
|
return nil
|
data/lib/moxml/context.rb
CHANGED
|
@@ -24,6 +24,13 @@ module Moxml
|
|
|
24
24
|
# up to the old 65,536 wholesale-clear valve which also
|
|
25
25
|
# destroyed identity for live wrappers when it fired.
|
|
26
26
|
@wrappers = WEAK_WRAPPERS ? ObjectSpace::WeakMap.new : {}.compare_by_identity
|
|
27
|
+
# Static per context (the registry flavor never changes).
|
|
28
|
+
@wrappers_strong = !WEAK_WRAPPERS
|
|
29
|
+
# Resolved on first registration — the adapter-resolution
|
|
30
|
+
# chain ran per mint and the decision never changes for a
|
|
31
|
+
# context's adapter (re-configuring an adapter mid-context is
|
|
32
|
+
# unsupported: minted wrappers already carry the old one).
|
|
33
|
+
@register_wrappers = nil
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def wrapper_for(native)
|
|
@@ -31,15 +38,19 @@ module Moxml
|
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def register_wrapper(native, wrapper)
|
|
34
|
-
# Adapter opt-in
|
|
35
|
-
# access (libxml mints fresh
|
|
36
|
-
#
|
|
37
|
-
# default is opt-in.
|
|
38
|
-
|
|
41
|
+
# Adapter opt-in, resolved once (see initialize): adapters
|
|
42
|
+
# whose natives are recreated per access (libxml mints fresh
|
|
43
|
+
# Ruby objects for the same C node) opt out so the map does
|
|
44
|
+
# not accumulate dead entries; the default is opt-in.
|
|
45
|
+
if @register_wrappers.nil?
|
|
46
|
+
@register_wrappers =
|
|
47
|
+
@config&.adapter&.wrappers_recyclable? != false
|
|
48
|
+
end
|
|
49
|
+
return unless @register_wrappers
|
|
39
50
|
|
|
40
51
|
# The strong fallback (Opal) needs the wholesale-clear valve;
|
|
41
52
|
# the WeakMap registry is self-cleaning.
|
|
42
|
-
@wrappers.clear if @
|
|
53
|
+
@wrappers.clear if @wrappers_strong && @wrappers.size >= 65_536
|
|
43
54
|
@wrappers[native] = wrapper
|
|
44
55
|
end
|
|
45
56
|
|
data/lib/moxml/element.rb
CHANGED
|
@@ -112,8 +112,11 @@ module Moxml
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def attributes
|
|
115
|
+
# Primed like Node.wrap: the adapter and type are known at
|
|
116
|
+
# mint, so the first name/value/attribute? access skips the
|
|
117
|
+
# context hop and the adapter's type probe.
|
|
115
118
|
@attributes ||= adapter.attributes(@native).map do |attr|
|
|
116
|
-
a = Attribute.new(attr, context)
|
|
119
|
+
a = Attribute.new(attr, context, adapter, :attribute)
|
|
117
120
|
a.parent_node = self
|
|
118
121
|
a
|
|
119
122
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -63,6 +63,19 @@
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
describe "qname create + namespace assignment (issue #208)" do
|
|
67
|
+
it "does not double the prefix when the name is already qualified" do
|
|
68
|
+
doc = ctx.parse(%(<r xmlns:p="urn:p"/>))
|
|
69
|
+
el = doc.create_element("p:c")
|
|
70
|
+
ns = doc.root.in_scope_namespaces.find { |n| n.prefix == "p" }
|
|
71
|
+
el.namespace = ns
|
|
72
|
+
doc.root.add_child(el)
|
|
73
|
+
out = doc.to_xml
|
|
74
|
+
expect(out).to include("p:c")
|
|
75
|
+
expect(out).not_to include("p:p:c")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
66
79
|
describe "namespaces contract (issue #198 comment)" do
|
|
67
80
|
it "returns the in-scope map including ancestor declarations" do
|
|
68
81
|
doc = ctx.parse(%(<root xmlns:p="urn:p" xmlns="urn:d"><p:c plain="1"/></root>))
|
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.34
|
|
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-09-
|
|
11
|
+
date: 2026-09-14 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
|