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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fcb33efd8969194833ce7ee6b7e3e3bfa6517ec7d216028407d28fbc95b1641
4
- data.tar.gz: ec2239b9b5501b208331ce5874764ad99cad357bacec08af6cc47d1f3aa69aff
3
+ metadata.gz: d43dcd89303022c12889585b26d38ab3bf199046d869b6412d45c6f3f9249347
4
+ data.tar.gz: e09d8585d90c5a2765040aaf4060bedaececca5228fe7808c11841088415d2e0
5
5
  SHA512:
6
- metadata.gz: 18526fb6430f0231b187e888104c4f2fef7e38999d1015d8324ea53b90339de4c0d5d58cb4b591380b5d6cca704014fcb30ca5429f5abeff2a027217e1d47e54
7
- data.tar.gz: 206cfe96f4bcac080a134092dac6f305d931d9d589c0dd544cf47e6bceb39d77d023f3b6e6c80deaeba94d10997840c90b583770d8c9d35e81dca65706354c0c
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
- element.add_namespace_definition(prefix, uri.to_s)
460
- element.name = "#{prefix}:#{element.name.split(':', 2)[-1]}"
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
- else
635
- # The binding reports the root element as parentless; the
636
- # moxml contract roots at the document.
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
@@ -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
@@ -167,13 +167,16 @@ module Moxml
167
167
  end
168
168
 
169
169
  prefix = ns.prefix
170
- # attributes don't have attributes but can have a namespace prefix
171
- if element.is_a?(::Ox::Element)
172
- set_attribute(element, ns.expanded_prefix,
173
- ns.uri)
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
@@ -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
- element.name = "#{prefix}:#{element.name}"
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
 
@@ -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
- if name == "xmlns" || name.start_with?("xmlns:")
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 == "xmlns" || name.start_with?("xmlns:")
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. Adapters whose natives are recreated per
35
- # access (libxml mints fresh Ruby objects for the same C node)
36
- # opt out so the map does not accumulate dead entries; the
37
- # default is opt-in.
38
- return if @config&.adapter&.wrappers_recyclable? == false
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 @wrappers.is_a?(Hash) && @wrappers.size >= 65_536
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.33"
4
+ VERSION = "0.5.34"
5
5
  end
@@ -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.33
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-13 00:00:00.000000000 Z
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