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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a421da0e82158a69de2d25328346153be526d74c6d2bc2bad732427763b175c1
4
- data.tar.gz: c9d982a98b2ce4984b3ee8659e89f2a4d86484d7e39fec4c04221bd2767473eb
3
+ metadata.gz: d182614a13bd3934c6d6dc1bb9b137015dc1daa124aeac24df40246ea3109103
4
+ data.tar.gz: 51d96b5f964b185772d4378913c5e48e4775c41cf5fd1a3e5cf942576fa33183
5
5
  SHA512:
6
- metadata.gz: 2a3c9a536530f058a8c0a7721a3f192d89be195df277e959d746f2b6360db2da3587c2467fc21222aff85b96c8c3483a6a59406554804e073b25336c6d5dfe17
7
- data.tar.gz: 750f9751134a3c077bd103edb9b18b0de7ea179d0c27839f1dff22654d372d7eb989a55c154c09ee155ec64d386547885013320eefa4044cb7dccc994d309806
6
+ metadata.gz: f4e9ae7331917e4fb295aa541028cb233d889127e4e32982d24ad1d696df668f52d223b70a1c6f2e96360f72138abf36a2eb055f15b9f48c069cb7b421191cfe
7
+ data.tar.gz: '0362854ca321c56e434a0f030232088d200d948dfe231c2d2cf4d2017c3af7316610e3f18ece3ca85a9a9a2c0d98f431c9f3bb465f186fe39ae23b63ea2e9a7b'
@@ -13,6 +13,13 @@ on:
13
13
  repository_dispatch:
14
14
  types: [ release-passed ]
15
15
 
16
+ # One live run per workflow+event+ref: a superseded push to the
17
+ # same PR/branch cancels its predecessors instead of queueing
18
+ # behind them (the runner queue otherwise stacks generations).
19
+ concurrency:
20
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
21
+ cancel-in-progress: true
22
+
16
23
  jobs:
17
24
  rake:
18
25
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
@@ -6,6 +6,13 @@ on:
6
6
  tags: [ v* ]
7
7
  pull_request:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -6,6 +6,13 @@ on:
6
6
  tags: [ v* ]
7
7
  pull_request:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -5,6 +5,13 @@ on:
5
5
  branches: [ main ]
6
6
  pull_request:
7
7
 
8
+ # One live run per workflow+event+ref: a superseded push to the
9
+ # same PR/branch cancels its predecessors instead of queueing
10
+ # behind them (the runner queue otherwise stacks generations).
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
8
15
  permissions:
9
16
  contents: read
10
17
 
@@ -6,6 +6,13 @@ on:
6
6
  pull_request:
7
7
  workflow_dispatch:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -47,8 +47,11 @@ module Moxml
47
47
 
48
48
  def parse_fragment(xml, _context = nil)
49
49
  doc = parse("<m>#{xml}</m>")
50
- root = root(doc)
51
- root ? children(root) : []
50
+ # doc.root is a wrapper (adapter root overrides answer
51
+ # patched/unpatched natives indifferently); children() and
52
+ # every adapter below expect natives.
53
+ root_node = doc.root
54
+ root_node ? children(root_node.native) : []
52
55
  end
53
56
 
54
57
  # Streaming incremental parse (leptris engine): yields each
@@ -146,6 +149,22 @@ module Moxml
146
149
  nil
147
150
  end
148
151
 
152
+ # Canonical node identity: adapters carrying more than one
153
+ # Ruby representation over one C node answer the representation
154
+ # the tree walk mints, so every wrap path resolves one wrapper
155
+ # per logical node (moxml#311). Default: the node is canonical.
156
+ def canonical_native(node)
157
+ node
158
+ end
159
+
160
+ # Element-only children: adapters whose engine carries the
161
+ # face override (leptris C filter, nokogiri native); the
162
+ # default type-filters the native child list so non-element
163
+ # wrappers never mint on element-only traversals.
164
+ def element_children(node)
165
+ children(node).select { |child| node_type(child) == :element }
166
+ end
167
+
149
168
  # Prefixed-attribute value fast path: adapters whose engine
150
169
  # resolves expanded-name (uri, local) lookups natively answer
151
170
  # the value here; others fall back to the resolver's
@@ -386,6 +405,13 @@ namespace_validation_mode: :strict)
386
405
  true
387
406
  end
388
407
 
408
+ # Serialization of the node's children (Element#inner_xml).
409
+ # Built over the children/serialize contract; adapters with a
410
+ # native inner-serialization face may override.
411
+ def inner_xml(node)
412
+ children(node).map { |child| serialize(child) }.join
413
+ end
414
+
389
415
  # Whether the engine offers a bulk materialization path for
390
416
  # Materializer (issue #132). When true, the adapter gets
391
417
  # #materialize_fields(native, buffers, &block) — fill the
@@ -48,7 +48,7 @@ module Moxml
48
48
  # equal?-based exclusion (canon's document-element skip)
49
49
  # silently breaks (issue #219). Mint-on-miss converges
50
50
  # both accessors on the same native object.
51
- children.map! { |child| canonical_native(doc, child) } if NATIVE_READ_LAYER
51
+ children.map! { |child| native_twin(doc, child) } if NATIVE_READ_LAYER
52
52
  children
53
53
  end
54
54
 
@@ -100,6 +100,10 @@ module Moxml
100
100
  NN_NEXT_SIBLING = NN.instance_method(:next_sibling)
101
101
  NN_CONTENT = NN.instance_method(:content)
102
102
  NN_CHILDREN = NN.instance_method(:children)
103
+ # The C element filter — captured because ElementBehavior's
104
+ # element_children alias shadows the face on Identity natives
105
+ # (a plain dispatch would recurse into the wrapper method).
106
+ NN_ELEMENT_CHILDREN = NN.instance_method(:element_children)
103
107
  NN_NODE_TYPE = NN.instance_method(:node_type)
104
108
  NN_DOCUMENT = NN.instance_method(:document)
105
109
  NN_ADD_CHILD = NN.instance_method(:add_child)
@@ -168,6 +172,18 @@ module Moxml
168
172
  BINDING_OF_CAP = 8_192
169
173
 
170
174
  class << self
175
+ # WeakMap has no #clear — 0.5.100's valve crashed every parse
176
+ # crossing the cap on 3.4+ (NoMethodError). Replace the
177
+ # registry wholesale, type-preserving: the WeakMap flavor
178
+ # stays weak, the strong Hash keeps compare_by_identity.
179
+ def fresh_node_registry
180
+ if Context::WEAK_WRAPPERS
181
+ ObjectSpace::WeakMap.new
182
+ else
183
+ {}.compare_by_identity
184
+ end
185
+ end
186
+
171
187
  # native -> bridged binding node. The bridge object is
172
188
  # identity-stable for the native's lifetime (the binding's
173
189
  # per-document wrap cache), so repeat bridges — the write
@@ -187,7 +203,7 @@ module Moxml
187
203
  doc = doc_for(node)
188
204
  ptr = ::FFI::Pointer.new(node.address)
189
205
  bridged = ::Leptris::XML::Node.wrap(ptr, doc)
190
- @binding_of.clear if @binding_of.size >= BINDING_OF_CAP
206
+ @binding_of = fresh_node_registry if @binding_of.size >= BINDING_OF_CAP
191
207
  @binding_of[node] = bridged unless bridged.nil?
192
208
  bridged
193
209
  end
@@ -428,7 +444,7 @@ module Moxml
428
444
  end
429
445
 
430
446
  def record_native_doc(root_native, doc)
431
- @native_doc_roots.clear if @native_doc_roots.size >= NATIVE_DOC_ROOTS_CAP
447
+ @native_doc_roots = fresh_node_registry if @native_doc_roots.size >= NATIVE_DOC_ROOTS_CAP
432
448
  @native_doc_roots[root_native] = doc
433
449
  end
434
450
 
@@ -1385,6 +1401,19 @@ module Moxml
1385
1401
  end
1386
1402
  end
1387
1403
 
1404
+ # Element-only children through the engine-level filter: the
1405
+ # C face mints element-kind natives directly (klass-children),
1406
+ # so interleaved text/comment nodes cost nothing. Entity
1407
+ # markers are text-kind — the element filter excludes them by
1408
+ # construction, no marker split needed.
1409
+ def element_children(node)
1410
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1411
+ return NN_ELEMENT_CHILDREN.bind_call(node).to_a
1412
+ end
1413
+
1414
+ to_binding(node).element_children
1415
+ end
1416
+
1388
1417
  def children(node, entity_bearing: false)
1389
1418
  # NATIVE_READ_LAYER gates the whole keep-path: it consults
1390
1419
  # NN_DOCUMENT (defined only with the native layer), and
@@ -1511,7 +1540,7 @@ module Moxml
1511
1540
  # accessor converges on one native per node (issue #219).
1512
1541
  # Binding natives (Attribute/Attr, synthetic wrappers) and
1513
1542
  # docless nodes pass through unchanged.
1514
- def canonical_native(doc, node)
1543
+ def native_twin(doc, node)
1515
1544
  return node unless node.is_a?(::Leptris::XML::Element)
1516
1545
 
1517
1546
  cache = doc.native_cache
@@ -1584,10 +1613,25 @@ module Moxml
1584
1613
  [copied, pointers]
1585
1614
  end
1586
1615
 
1587
- # The binding reports the root element as parentless; the
1588
- # moxml contract roots at the document.
1616
+ # The binding reports the root element and the document-level
1617
+ # parts as parentless; the moxml contract roots them at the
1618
+ # document. DocType carries no #parent at all (plain
1619
+ # c_ptr+document value object) and a parentless PI is a
1620
+ # document-level one by construction — in-tree parts have
1621
+ # element parents.
1589
1622
  def root_parent(node)
1590
- node.parent || (node.document&.root == node ? node.document : nil)
1623
+ return node.document if node.is_a?(::Leptris::XML::DocType)
1624
+
1625
+ parent = node.parent
1626
+ return parent if parent
1627
+
1628
+ doc = node.document
1629
+ return nil unless doc
1630
+
1631
+ return doc if doc.root == node ||
1632
+ node.is_a?(::Leptris::XML::ProcessingInstruction)
1633
+
1634
+ nil
1591
1635
  end
1592
1636
 
1593
1637
  def next_sibling(node)
@@ -1625,7 +1669,7 @@ module Moxml
1625
1669
  # Through the same address-keyed cache as the children
1626
1670
  # path: NativeNode.from alone mints a fresh object per
1627
1671
  # call on 1.9.163.x, splitting the wrappers (issue #219).
1628
- native = canonical_native(document, r)
1672
+ native = native_twin(document, r)
1629
1673
  record_native_doc(native, document)
1630
1674
  return native
1631
1675
  end
@@ -1686,8 +1730,8 @@ module Moxml
1686
1730
  # convergence (#219) hands every accessor — children
1687
1731
  # included — the SAME native, so the wrapper identity
1688
1732
  # holds across later traversals.
1689
- canonical_native(doc,
1690
- ::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
1733
+ native_twin(doc,
1734
+ ::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
1691
1735
  end
1692
1736
 
1693
1737
  def attributes(element)
@@ -1839,6 +1883,7 @@ module Moxml
1839
1883
  node = to_binding(node)
1840
1884
  new_node = to_binding(new_node)
1841
1885
  end
1886
+ pin_foreign_owner_document(node, new_node)
1842
1887
  # A PI inserted before the root lives at document level in
1843
1888
  # libleptris's model, not in the element tree.
1844
1889
  if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
@@ -1864,6 +1909,7 @@ module Moxml
1864
1909
  node = to_binding(node)
1865
1910
  new_node = to_binding(new_node)
1866
1911
  end
1912
+ pin_foreign_owner_document(node, new_node)
1867
1913
  return node.add_next_sibling(new_node) if node.is_a?(::Leptris::XML::Element)
1868
1914
 
1869
1915
  # Non-element receivers (Text/Comment/CDATA): the binding's
@@ -1935,7 +1981,19 @@ module Moxml
1935
1981
  node = to_binding(node)
1936
1982
  new_node = to_binding(new_node)
1937
1983
  end
1938
- return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
1984
+ if node.is_a?(::Leptris::XML::Element)
1985
+ # NOT Element#replace: it detaches via
1986
+ # leptris_element_remove_child, which leaves the replaced
1987
+ # node's sibling links pointing into the live tree. A later
1988
+ # append of that node splices the stale run under the new
1989
+ # parent, and when the run contains the append target the
1990
+ # tree cycles (standoc term cleanup does
1991
+ # t << n.replace(t2)). Insert + unlink instead: the unlink
1992
+ # path clears the sibling links.
1993
+ node.add_next_sibling(new_node)
1994
+ node.unlink
1995
+ return new_node
1996
+ end
1939
1997
 
1940
1998
  # libleptris only offers element-anchored insertion, so a
1941
1999
  # content node (text/comment/CDATA/PI) is replaced by
@@ -2118,6 +2176,29 @@ module Moxml
2118
2176
  engine_xpath(node, expression, namespaces)
2119
2177
  end
2120
2178
 
2179
+ # Canonical node identity (moxml#311): the native layer and
2180
+ # the binding layer hold separate Ruby objects over one C
2181
+ # node, and only the native cache carries the tree-walk
2182
+ # identity the @parent_node invalidation chain rides on.
2183
+ # Resolve binding-layer nodes (xpath results, engine results)
2184
+ # to the walked instance when one exists — a memoized
2185
+ # children holder implies a prior walk, so a cache miss means
2186
+ # no holder exists and the binding node is safe as-is.
2187
+ def canonical_native(node)
2188
+ return node unless NATIVE_READ_LAYER
2189
+ return node if node.is_a?(::Leptris::XML::NativeNode)
2190
+ return node unless node.is_a?(::Leptris::XML::Node)
2191
+
2192
+ doc = node.document
2193
+ return node if doc.nil?
2194
+
2195
+ # Only a contract-carrying hit is the walked canonical —
2196
+ # plain NN twins (entity references, #219 miss-mints) have
2197
+ # no Identity class and keep the binding representation.
2198
+ hit = doc.native_cache[node.c_ptr.address]
2199
+ hit.is_a?(::Moxml::Node) ? hit : node
2200
+ end
2201
+
2121
2202
  def at_xpath(node, expression, namespaces = {})
2122
2203
  native = native_xpath(node, expression, namespaces, first_only: true)
2123
2204
  return native unless native.nil?
@@ -2138,7 +2219,22 @@ module Moxml
2138
2219
  node = to_binding(node)
2139
2220
  end
2140
2221
  return nil unless native_context_node?(node)
2141
- return nil unless native_expression?(expression)
2222
+ # Hash-bound xmlns rides native on probing bindings
2223
+ # (leptris resolves the reserved prefix from the hash);
2224
+ # empty mappings (xmlns="") select no-namespace elements
2225
+ # and stay on the Ruby engine — the C pairs-set rejects
2226
+ # empty URIs. Everything else keeps the expression-keyed
2227
+ # gate.
2228
+ return nil unless (namespaces.is_a?(Hash) &&
2229
+ namespaces["xmlns"].to_s.length.positive? &&
2230
+ xmlns_native?) ||
2231
+ native_expression?(expression)
2232
+ # The leptris engine mishandles namespace-prefixed node
2233
+ # tests: "m:*" matches elements in ANY namespace and
2234
+ # "m:name" matches none, regardless of the binding. Route
2235
+ # namespace-bound queries to the Ruby engine until the
2236
+ # engine grows correct prefix resolution.
2237
+ return nil if namespaces && !namespaces.empty?
2142
2238
  # The binding reports the root element parentless while moxml
2143
2239
  # roots it at the document, so parent-axis queries from the
2144
2240
  # root element keep the Ruby engine.
@@ -2194,7 +2290,8 @@ module Moxml
2194
2290
  # and frees in one C dispatch — no NodeSet container, no
2195
2291
  # AutoPointer (leptris-ruby TODO.perf/16). The module
2196
2292
  # object back means a scalar, whose wrap (and free)
2197
- # stays with wrap_xpath_first_result.
2293
+ # stays with wrap_xpath_first_result. Identity resolution
2294
+ # happens at the at_xpath boundary (bridge_xpath_node).
2198
2295
  return ::Leptris::XML::Searchable.wrap_xpath_first_result(
2199
2296
  document, result_ptr
2200
2297
  )
@@ -2273,6 +2370,28 @@ module Moxml
2273
2370
  # xmlns:name is a nokogiri-compat convention addressing
2274
2371
  # elements in the default namespace; only the Ruby engine
2275
2372
  # implements it.
2373
+ # One-time capability probe: does the binding's native engine
2374
+ # resolve the reserved xmlns prefix from the namespace hash?
2375
+ # Memoized class-level — the answer never changes for an
2376
+ # install. Only hash-bound xmlns queries may ride native
2377
+ # (the gate below is expression-keyed and cannot see the
2378
+ # call's namespaces); unbound xmlns stays on the Ruby engine,
2379
+ # whose reserved-prefix branch matches default-namespace
2380
+ # elements.
2381
+ def xmlns_native?
2382
+ return @xmlns_native unless @xmlns_native.nil?
2383
+
2384
+ @xmlns_native = begin
2385
+ probe = parse('<r xmlns="urn:p"><a>1</a></r>', {}, nil)
2386
+ raw = to_binding(probe.root.native)
2387
+ res = raw.xpath("./xmlns:a", { "xmlns" => "urn:p" })
2388
+ list = res.respond_to?(:to_a) ? res.to_a : [res].compact
2389
+ list.size == 1 && list.first.text.to_s == "1"
2390
+ rescue StandardError
2391
+ false
2392
+ end
2393
+ end
2394
+
2276
2395
  def uses_xmlns_prefix?(ast)
2277
2396
  return true if ast.type == :test && ast.value[:namespace] == "xmlns"
2278
2397
 
@@ -1099,8 +1099,9 @@ module Moxml
1099
1099
  target_doc = doc || (element.is_a?(::LibXML::XML::Node) ? element.doc : nil)
1100
1100
 
1101
1101
  if target_doc
1102
- # Use deep import to ensure all descendants are included
1103
- imported = target_doc.import(child, true)
1102
+ # libxml-ruby 6.0 dropped import's deep flag; the import
1103
+ # walks the subtree either way.
1104
+ imported = target_doc.import(child)
1104
1105
  element << imported
1105
1106
  else
1106
1107
  # No target document - create a deep copy of the node instead
@@ -318,6 +318,10 @@ module Moxml
318
318
  node.children
319
319
  end
320
320
 
321
+ def element_children(node)
322
+ node.element_children
323
+ end
324
+
321
325
  def adjacent_to_entity_reference?(node)
322
326
  node.previous_sibling.is_a?(::Nokogiri::XML::EntityReference) ||
323
327
  node.next_sibling.is_a?(::Nokogiri::XML::EntityReference)
data/lib/moxml/cdata.rb CHANGED
@@ -19,5 +19,8 @@ module Moxml
19
19
  def name
20
20
  "#cdata-section"
21
21
  end
22
+
23
+ # Nokogiri-compatible: renaming non-element nodes is a no-op
24
+ def name=(_value); end
22
25
  end
23
26
  end
data/lib/moxml/comment.rb CHANGED
@@ -21,5 +21,8 @@ module Moxml
21
21
  def name
22
22
  "comment"
23
23
  end
24
+
25
+ # Nokogiri-compatible: renaming non-element nodes is a no-op
26
+ def name=(_value); end
24
27
  end
25
28
  end
data/lib/moxml/context.rb CHANGED
@@ -74,6 +74,18 @@ module Moxml
74
74
  end
75
75
  end
76
76
 
77
+ # Children-memo generation: bumped by every tree mutation so
78
+ # per-wrapper children memos can self-invalidate even when the
79
+ # same logical node is reachable through multiple wrapper
80
+ # representations (native vs binding duality).
81
+ def children_generation
82
+ @children_generation ||= 0
83
+ end
84
+
85
+ def bump_children_generation
86
+ @children_generation = children_generation + 1
87
+ end
88
+
77
89
  def namespace_scope_generation
78
90
  @namespace_scope_generation
79
91
  end
@@ -16,6 +16,38 @@ module Moxml
16
16
  end
17
17
 
18
18
  def root=(element)
19
+ owner = element.is_a?(Node) && element.document
20
+ # Only a genuinely ATTACHED foreign element adopts: the document
21
+ # face answers arbitrary Document wrappers for FLOATING elements
22
+ # (leptris: a stale mint-time document; ox: an Element), and
23
+ # adopting a floating element splices its (empty) copy as root —
24
+ # the caller keeps appending to the detached original and the
25
+ # tree serializes empty (lutaml-model's builder lost every
26
+ # child this way). Attached means parented, or another
27
+ # document's current root.
28
+ attached = !element.parent.nil? ||
29
+ (owner.is_a?(Document) && owner.root.equal?(element))
30
+ if owner.is_a?(Document) && attached &&
31
+ !owner.equal?(self) && element.context.equal?(context)
32
+ # libleptris refuses a root owned by another document (the
33
+ # engine's set_root ownership check), while Nokogiri adopts.
34
+ # Cross-document CHILD attaches are supported, so build the
35
+ # root here, mirror its declarations and attributes, and
36
+ # splice the foreign subtree child-by-child — each attach
37
+ # rides adapter add_child, which pins the owner document
38
+ # against GC (issue #304).
39
+ new_root = create_element(element.name)
40
+ element.attribute_pairs.each { |k, v| new_root[k] = v }
41
+ element.declared_namespaces.each do |prefix, uri|
42
+ new_root.add_namespace(prefix, uri)
43
+ end
44
+ adapter.set_root(@native, new_root.native)
45
+ new_root.parent_node = self
46
+ element.children.to_a.each { |child| new_root.add_child(child) }
47
+ context.bump_children_generation
48
+ invalidate_children_cache!
49
+ return
50
+ end
19
51
  adapter.set_root(@native, element.native)
20
52
  element.parent_node = self
21
53
  invalidate_children_cache!
@@ -113,6 +145,14 @@ module Moxml
113
145
  Wrappers::EntityReference.new(native, context)
114
146
  end
115
147
 
148
+ # Nokogiri-compatible: Document#name returns "document"
149
+ def name
150
+ "document"
151
+ end
152
+
153
+ # Nokogiri-compatible: renaming a document node is a no-op
154
+ def name=(_value); end
155
+
116
156
  def add_child(node)
117
157
  node = prepare_node(node)
118
158
 
data/lib/moxml/element.rb CHANGED
@@ -159,6 +159,40 @@ module Moxml
159
159
  end
160
160
  end
161
161
 
162
+ # Nokogiri-compatible: delete(name) removes the attribute
163
+ def delete(name)
164
+ remove_attribute(name)
165
+ end
166
+
167
+ # Nokogiri-compatible: element-only children. The adapter seam
168
+ # filters at the engine level where the binding carries the face
169
+ # (leptris bulk element children, nokogiri native), so interleaved
170
+ # text/comment nodes never mint wrappers on hot traversals.
171
+ # Memo rides the children generation (#310's mutation clock).
172
+ def elements
173
+ @elements = nil if @elements_gen != context.children_generation
174
+ @elements_gen = context.children_generation
175
+ @elements ||= NodeSet.new(adapter.element_children(@native), context, self)
176
+ end
177
+ alias element_children elements
178
+
179
+ # Nokogiri-compatible: children= replaces the entire child list
180
+ def children=(node_or_nodes)
181
+ natives = case node_or_nodes
182
+ when String
183
+ wrapper = "_moxml_children_#{Process.pid}_#{object_id}"
184
+ context.parse("<#{wrapper}>#{node_or_nodes}</#{wrapper}>")
185
+ .root.children.map(&:native)
186
+ when Moxml::Node then [node_or_nodes.native]
187
+ when Array, NodeSet then node_or_nodes.map(&:native)
188
+ else
189
+ raise ArgumentError, "children= accepts String, Node, or Array"
190
+ end
191
+ adapter.replace_children(@native, natives)
192
+ context.bump_children_generation
193
+ invalidate_children_cache!
194
+ end
195
+
162
196
  def remove_attribute(name)
163
197
  # Both remove paths end in invalidate_attribute_cache!, which
164
198
  # bumps the generation.
@@ -233,6 +267,19 @@ module Moxml
233
267
  invalidate_namespace_cache!
234
268
  end
235
269
 
270
+ # Nokogiri-compatible: bind the element to +uri+ as its DEFAULT
271
+ # namespace — the name drops any prefix and the declaration lands
272
+ # on this element. Children keep their own bindings. Skip when an
273
+ # identical default namespace is already in scope: declaring again
274
+ # serializes a duplicate xmlns attribute. (add_namespace is the
275
+ # primitive: namespace=({nil => uri}) declares twice on leptris.)
276
+ def default_namespace=(uri)
277
+ self.name = name.split(":", 2)[-1] if name.to_s.include?(":")
278
+ return if namespace_prefix.to_s.empty? && namespace_uri.to_s == uri.to_s
279
+
280
+ add_namespace(nil, uri.to_s)
281
+ end
282
+
236
283
  # All namespaces IN SCOPE for this element — its own
237
284
  # declarations plus everything inherited from ancestors —
238
285
  # matching the Nokogiri #namespaces contract consumers port
@@ -296,6 +343,8 @@ module Moxml
296
343
  invalidate_children_cache!
297
344
  end
298
345
 
346
+ alias content= text=
347
+
299
348
  def inner_text
300
349
  text = raw_inner_text
301
350
  entity_bearing? ? adapter.restore_entities(text) : text