moxml 0.5.150 → 0.5.152

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: 73b67b633933c60095bdeafd744a8f7a827066edf478245e9c2f49aadf1d75e6
4
- data.tar.gz: 92320b13e8bb745cd5b650e85d4fa7b8397e082871a7c255cb61f637595c6048
3
+ metadata.gz: 5a384caeb57d40002410d3ebfb5ef9a22eed2793caab5db918d9bfeacaacbdee
4
+ data.tar.gz: 166f445cae5e401f0cf5be6a3f766eb9232809fe1217e3e90e46f2e315afce8e
5
5
  SHA512:
6
- metadata.gz: c34b4df4cf09797b5a28ec909e0df41c970897676154fa1b66dc9ebbd18cdccb13408b83f5647c44d41cfda7ebcbf09571314d0bfe71b572a58dde972e78994b
7
- data.tar.gz: d926c442a8b0dc0f9d159f27f42772a3af48358fbbd67d62d7cec1cdd51c609110def03466fa93c5b95b2eccfdaa7ab34e26c818b30b2651a996b9879c63d699
6
+ metadata.gz: cc19f58546f479a713b3039295b2eac40b5c83166dacc77af0017e83b40f2d3f8bffb028767f744ef29dae5688c3c5d3c34a248e9d3f75bede910b444c04b675
7
+ data.tar.gz: 9f9a64184feb0ba0a0eba61300a014339b81c7a58d69a1b1ff0dd4610abb7c239a6184e260253542f4643410fcfe3b15e54470b72cdb72283e7bf5a85f3d1639
@@ -21,7 +21,7 @@ validation target, never a stand-in.
21
21
 
22
22
  | `lutaml/lutaml-model` | — | 6213+ examples green on the released pair
23
23
 
24
- | `lutaml/canon` | — | 2573 examples, 0 failures locally; in CI the Windows leg fails one JSON-engine parity example (`yaml_engine_parity_spec.rb:123`, the resolved json gem's duplicate-key verdict) — unrelated to XML processing, owner-held in canon
24
+ | `lutaml/canon` | — | 2573 examples, 0 failures (a transient Windows failure of one JSON-engine parity example — yeptris 0.6.34.2's undefined `MARSHAL_STRICT_DUP_KEYS` under json 3 — cleared with yeptris 0.6.34.3)
25
25
 
26
26
  | `metanorma/metanorma-standoc` | ghostscript | 627 examples green in CI on main (the boilerplate-fixture failure seen during the 04/05 validation was resolved by the 06 adapter fixes; a few local-only failures still need generated assets)
27
27
 
@@ -42,7 +42,7 @@ a declared, decided lane.
42
42
  |===
43
43
  | Consumer | Use | Disposition
44
44
 
45
- | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Facade capability shipped**: `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column. canon adopts it in canon#281, which is owner-held (moxml does not merge canon PRs).
45
+ | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Retired** (canon#281, merged): `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column; canon rides the facade.
46
46
 
47
47
  | canon | c14n native delegation (`doc.native.c14n`) | **Decided lane**: canon owns the c14n reference implementation and byte-probes the engine against its Ruby path; it is the backend behind moxml's own native delegation.
48
48
 
@@ -580,6 +580,13 @@ namespace_validation_mode: :strict)
580
580
  false
581
581
  end
582
582
 
583
+ # Whether the mint pre-writes the consumer's contract ivars
584
+ # in C (create_primed_element), letting Document#create_element
585
+ # skip its Ruby prime pass. Base: never.
586
+ def mints_primed?
587
+ false
588
+ end
589
+
583
590
  # Whether add_child can keep tracking the same native —
584
591
  # adapters that may recreate the node on attach (libxml's
585
592
  # doc.root=) override to false so the wrapper refresh path
@@ -588,6 +595,15 @@ namespace_validation_mode: :strict)
588
595
  false
589
596
  end
590
597
 
598
+ # Whether attach_child records the wrapper-side parent link
599
+ # (@parent_node) on the attached node. Adapters whose natives
600
+ # cannot resolve a parent through the adapter layer (raw-text
601
+ # natives, re-mints) need the link for remove/replace to find
602
+ # the tree — leave it on unless the adapter proves otherwise.
603
+ def attach_sets_parent_link?
604
+ true
605
+ end
606
+
591
607
  # Whether a BARE-name attribute READ addresses only the
592
608
  # no-namespace attribute (qualified-name semantics) — the
593
609
  # gate for Element#[]'s fast path (bare_attr_value).
@@ -71,6 +71,18 @@ module Moxml
71
71
  defined?(::Leptris::XML::NativeNode) &&
72
72
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
73
73
 
74
+ # leptris-ruby#529: the whole XPath node-set result as an Array
75
+ # of registered natives in ONE C crossing (Document#
76
+ # xpath_result_rows_bulk). The eager lane below crosses once and
77
+ # mints one wrapper per row on iterate-every-result. Probed on
78
+ # the Document binding class (the class-level respond_to? for
79
+ # instance surfaces) AND on the compiled ext face — a fresh
80
+ # native_layer over a stale ext would NoMethodError mid-query.
81
+ BULK_XPATH_ROWS =
82
+ defined?(::Leptris::XML::Native) &&
83
+ ::Leptris::XML::Native.respond_to?(:xpath_result_rows_bulk) &&
84
+ ::Leptris::XML::Document.method_defined?(:xpath_result_rows_bulk)
85
+
74
86
  # leptris-ruby#460: set-text by document+address, kind-dispatched
75
87
  # in C — no binding twin mint per write. Face-probed, not
76
88
  # version-gated (the floor admits bindings without it).
@@ -78,6 +90,21 @@ module Moxml
78
90
  defined?(::Leptris::XML::Native) &&
79
91
  ::Leptris::XML::Native.respond_to?(:set_binding_text)
80
92
 
93
+ # leptris-ruby#532 (engine ride 1.9.379.x): Native.walk_columnar —
94
+ # a whole subtree's walk data ([names, handles, depths]; preorder,
95
+ # receiver at depth 0 — document/doctype receivers walk their
96
+ # children at depth 0) in ONE C crossing. The children face primes
97
+ # a per-document {native => children} map from that single call,
98
+ # so a consumer walk pays one crossing per cold subtree instead of
99
+ # one per node (leptris-ruby#527 measured the per-node
100
+ # children+name crossing at ~23% of cold-walk samples). Handles
101
+ # mint through the document's native-cache identity, so cached
102
+ # entries ARE the children_read wrappers. Face-probed, not
103
+ # version-gated.
104
+ COLUMNAR_WALK =
105
+ defined?(::Leptris::XML::Native) &&
106
+ ::Leptris::XML::Native.respond_to?(:walk_columnar)
107
+
81
108
  # 1.9.163.2's native layer returns UTF-8, unfrozen strings
82
109
  # (1.9.162.x answered ASCII-8BIT — the reads retagged with a
83
110
  # dup+force_encoding per call).
@@ -1193,14 +1220,29 @@ module Moxml
1193
1220
  # entry are correct; upstream bug filed). Track the state per
1194
1221
  # binding document and route XPath to the reference Ruby
1195
1222
  # engine until a child insertion rebuilds the engine index.
1223
+ #
1224
+ # FIXED upstream in libleptris 1.9.378 (leptris/leptris#1802):
1225
+ # every splice path invalidates the index, so the guard is
1226
+ # dead weight on modern engines — and it sat on the builder
1227
+ # hot path (one Monitor-synchronized attachment set plus a
1228
+ # document hop per add_child). Retirement is probed, not
1229
+ # version-gated: any build that passes the #519 repro opts
1230
+ # itself out, no moxml release needed.
1196
1231
  XPATH_INDEX_STALE = :xpath_index_stale
1197
1232
 
1233
+ # Attachment key for the columnar children map (COLUMNAR_WALK).
1234
+ COLUMNAR_CHILDREN = :columnar_children_map
1235
+
1198
1236
  def mark_xpath_index_stale(node)
1237
+ return if native_xpath_index_fixed?
1238
+
1199
1239
  doc = xpath_index_document(node)
1200
1240
  attachments.set(doc, XPATH_INDEX_STALE, true) if doc
1201
1241
  end
1202
1242
 
1203
1243
  def clear_xpath_index_stale(node)
1244
+ return if native_xpath_index_fixed?
1245
+
1204
1246
  doc = xpath_index_document(node)
1205
1247
  attachments.set(doc, XPATH_INDEX_STALE, false) if doc
1206
1248
  end
@@ -1241,6 +1283,8 @@ module Moxml
1241
1283
  end
1242
1284
 
1243
1285
  def set_root(doc, element)
1286
+ drop_columnar_children(doc)
1287
+ drop_columnar_children(element)
1244
1288
  to_binding(doc).root = to_binding(element)
1245
1289
  end
1246
1290
 
@@ -1319,6 +1363,19 @@ module Moxml
1319
1363
  true
1320
1364
  end
1321
1365
 
1366
+ # Identity-stable natives resolve their parent through the
1367
+ # adapter (remove/replace fall back to that resolution when
1368
+ # the wrapper-side link is unset), and every mutation bumps
1369
+ # the children generation — so recording the link on attach
1370
+ # only materializes a fourth ivar per attached node
1371
+ # (object-shape transition, ~6% of the build row). Adapters
1372
+ # whose natives cannot self-resolve keep the link via
1373
+ # Base#attach_sets_parent_link?; parse_fragment mints its own
1374
+ # lifetime chain (#245) either way.
1375
+ def attach_sets_parent_link?
1376
+ false
1377
+ end
1378
+
1322
1379
  # Our at_xpath falls back to the model's Ruby engine when the
1323
1380
  # native lane answers nothing — the engine's node-set XPath
1324
1381
  # index is stale after sibling insertion, so `at` must not be
@@ -1627,6 +1684,7 @@ module Moxml
1627
1684
  # re-indented). Raw-pointer walk over the root subtree — the
1628
1685
  # same batch child-pointer pattern the materializer uses.
1629
1686
  def strip_blank_text_nodes!(doc)
1687
+ drop_columnar_children(doc)
1630
1688
  binding_ffi = ::Leptris::XML::FFI
1631
1689
  root = binding_ffi.leptris_document_root(doc.c_ptr)
1632
1690
  return unless root && !root.null?
@@ -1762,6 +1820,27 @@ module Moxml
1762
1820
  (owner_doc || create_document).create_element(name.to_s)
1763
1821
  end
1764
1822
 
1823
+ # MINT_PRIMED: the compiled ext's create_element takes
1824
+ # optional (context, adapter) and pre-writes the contract
1825
+ # ivars in C — arity -1 vs the old 2. Self-gating: no
1826
+ # version floor, the probe reads the loaded ext.
1827
+ MINT_PRIMED = defined?(::Leptris::XML::NativeNode) &&
1828
+ ::Leptris::XML::NativeNode
1829
+ .method(:create_element).arity == -1
1830
+
1831
+ def mints_primed?
1832
+ MINT_PRIMED
1833
+ end
1834
+
1835
+ def create_primed_element(name, owner_doc:, context:, adapter:)
1836
+ validate_element_name(name)
1837
+ # One dispatch: the compiled mint IS the wrapper factory.
1838
+ # The binding's native_create_element hop added a Ruby frame
1839
+ # (plus a redundant to_s and nil-branch) around a C call
1840
+ # that takes the same four arguments.
1841
+ ::Leptris::XML::NativeNode.create_element(owner_doc, name.to_s, context, adapter)
1842
+ end
1843
+
1765
1844
  def create_native_text(content, owner_doc = nil)
1766
1845
  if NATIVE_MUTATIONS_COHERENT && owner_doc
1767
1846
  return owner_doc.native_create_text(content)
@@ -2103,6 +2182,8 @@ module Moxml
2103
2182
  # source. Answer the installed binding element; callers re-point
2104
2183
  # wrappers at it.
2105
2184
  def adopt_root(doc, element)
2185
+ drop_columnar_children(doc)
2186
+ drop_columnar_children(element)
2106
2187
  return nil unless NATIVE_ROOT_ADOPT
2107
2188
 
2108
2189
  binding_doc = to_binding(doc)
@@ -2141,6 +2222,18 @@ module Moxml
2141
2222
  # TextSegment reconstruction reads binding text nodes;
2142
2223
  # the wrapper's memo supplies the flag, so deriving it
2143
2224
  # here costs no C parent climb per call.
2225
+ # Plain children-of-node read (no entity-marker split): the
2226
+ # columnar map answers from the last cold-subtree crossing —
2227
+ # one walk_columnar primes every child list under the asked
2228
+ # node, so a top-down consumer walk pays one crossing at the
2229
+ # root and none below it (COLUMNAR_WALK above).
2230
+ if COLUMNAR_WALK && !entity_bearing
2231
+ hit = columnar_children_hit(node)
2232
+ return hit if hit
2233
+
2234
+ return prime_columnar_children(node)
2235
+ end
2236
+
2144
2237
  natives = if NATIVE_BULK_FIXED
2145
2238
  NN_CHILDREN.bind_call(node).to_a
2146
2239
  else
@@ -2195,6 +2288,80 @@ module Moxml
2195
2288
  end
2196
2289
  end
2197
2290
 
2291
+ # Map hit for a primed node (see COLUMNAR_WALK). Served dup'd so
2292
+ # no consumer can mutate the cached list in place.
2293
+ def columnar_children_hit(node)
2294
+ doc = doc_for(node)
2295
+ return nil unless doc
2296
+
2297
+ map = attachments.get(doc, COLUMNAR_CHILDREN)
2298
+ return nil unless map&.key?(node)
2299
+
2300
+ map[node].dup
2301
+ end
2302
+
2303
+ # One walk_columnar crossing over node's subtree; stores a
2304
+ # children list for every visited node and returns node's own
2305
+ # list. Preorder + depths rebuild the parent chain on a stack;
2306
+ # the receiver's child depth is read off the first row (the
2307
+ # binding walks document/doctype receivers' children at depth
2308
+ # 0). Docless detached subtrees answer the receiver's list
2309
+ # unprimed.
2310
+ def prime_columnar_children(node)
2311
+ _names, handles, depths =
2312
+ ::Leptris::XML::Native.walk_columnar(node)
2313
+ return [] if handles.empty?
2314
+
2315
+ child_depth =
2316
+ if handles[0].equal?(node)
2317
+ depths[0] + 1
2318
+ else
2319
+ depths[0]
2320
+ end
2321
+ doc = doc_for(node)
2322
+ map = doc ? {} : nil
2323
+ mine = []
2324
+ stack = []
2325
+ handles.each_with_index do |handle, i|
2326
+ depth = depths[i]
2327
+ stack[depth] = handle
2328
+ map[handle] = [] if map
2329
+ if depth >= child_depth && map
2330
+ parent = stack[depth - 1]
2331
+ map[parent] << handle if map.key?(parent)
2332
+ end
2333
+ mine << handle if depth == child_depth
2334
+ end
2335
+ attachments.set(doc, COLUMNAR_CHILDREN, map) if map
2336
+ mine
2337
+ end
2338
+
2339
+ # Structural seams drop the columnar map wholesale; the next
2340
+ # children read re-primes it (see COLUMNAR_WALK). Cheap no-op
2341
+ # when no map is live: one lock-free attachment probe.
2342
+ def drop_columnar_children(node)
2343
+ return unless COLUMNAR_WALK
2344
+
2345
+ doc =
2346
+ case node
2347
+ when ::Leptris::XML::NativeNode
2348
+ doc_for(node)
2349
+ when ::Leptris::XML::Document
2350
+ node
2351
+ else
2352
+ begin
2353
+ node.document
2354
+ rescue StandardError
2355
+ nil
2356
+ end
2357
+ end
2358
+ return if doc.nil?
2359
+
2360
+ return if attachments.get(doc, COLUMNAR_CHILDREN).nil?
2361
+
2362
+ attachments.delete(doc, COLUMNAR_CHILDREN)
2363
+ end
2364
+
2198
2365
  def native_keep_entity_refs?(node)
2199
2366
  return false unless NATIVE_READ_LAYER &&
2200
2367
  node.is_a?(::Leptris::XML::NativeNode)
@@ -2408,6 +2575,8 @@ module Moxml
2408
2575
  # callers fall back to create_element + set_attribute +
2409
2576
  # add_child when absent.
2410
2577
  def create_element_with_attrs(parent, name, attrs)
2578
+ drop_columnar_children(parent)
2579
+ validate_element_name(name)
2411
2580
  binding_parent = to_binding(parent)
2412
2581
  doc = binding_parent.document or return nil
2413
2582
  return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
@@ -2487,7 +2656,22 @@ module Moxml
2487
2656
  attr.name.to_s.dup.force_encoding("UTF-8")
2488
2657
  end
2489
2658
 
2659
+ # Self-gating by method_defined? — older exts fall through to
2660
+ # the to_binding bridge below.
2661
+ SET_ATTRIBUTE_NATIVE =
2662
+ NATIVE_READ_LAYER && NN.method_defined?(:set_attribute)
2663
+
2490
2664
  def set_attribute(element, name, value)
2665
+ # Native write face (leptris-ruby#204's nn_set_attribute):
2666
+ # gates + version bump + engine set in one C frame on the
2667
+ # TypedData self — the builder hot path skips the to_binding
2668
+ # bridge (FFI::Pointer + wrapper mint + registry) entirely.
2669
+ # Cache coherence stays at the resolver: callers invalidate
2670
+ # after this returns, exactly as for the bridge.
2671
+ if SET_ATTRIBUTE_NATIVE && element.is_a?(::Leptris::XML::NativeNode)
2672
+ return element.set_attribute(name.to_s, value.to_s)
2673
+ end
2674
+
2491
2675
  element = to_binding(element) if NATIVE_READ_LAYER
2492
2676
  element[name.to_s] = value.to_s
2493
2677
  end
@@ -2698,6 +2882,8 @@ module Moxml
2698
2882
  # on the source, so a living source can never outlive the
2699
2883
  # memory its reads still touch.
2700
2884
  def absorb_source_document(dest, owner)
2885
+ drop_columnar_children(dest)
2886
+ drop_columnar_children(owner)
2701
2887
  return nil unless NATIVE_DOC_ABSORB
2702
2888
  return :direct if attachments.get(dest, :absorbed_sources)&.include?(owner)
2703
2889
 
@@ -2743,6 +2929,8 @@ module Moxml
2743
2929
  end
2744
2930
 
2745
2931
  def add_child(parent, child)
2932
+ drop_columnar_children(parent)
2933
+ drop_columnar_children(child)
2746
2934
  result = add_child_impl(parent, child)
2747
2935
  # A successful child insertion rebuilds the engine's
2748
2936
  # document index; the upstream sibling-insert bug leaves it
@@ -2864,6 +3052,8 @@ module Moxml
2864
3052
  end
2865
3053
 
2866
3054
  def add_previous_sibling(node, new_node)
3055
+ drop_columnar_children(node)
3056
+ drop_columnar_children(new_node)
2867
3057
  result = add_previous_sibling_impl(node, new_node)
2868
3058
  mark_xpath_index_stale(node)
2869
3059
  result
@@ -2910,6 +3100,8 @@ module Moxml
2910
3100
  end
2911
3101
 
2912
3102
  def add_next_sibling(node, new_node)
3103
+ drop_columnar_children(node)
3104
+ drop_columnar_children(new_node)
2913
3105
  result = add_next_sibling_impl(node, new_node)
2914
3106
  mark_xpath_index_stale(node)
2915
3107
  result
@@ -2963,6 +3155,7 @@ module Moxml
2963
3155
  end
2964
3156
 
2965
3157
  def remove(node)
3158
+ drop_columnar_children(node)
2966
3159
  node = to_binding(node) if NATIVE_READ_LAYER
2967
3160
  case node
2968
3161
  when CustomizedLeptris::Declaration
@@ -2994,6 +3187,8 @@ module Moxml
2994
3187
  end
2995
3188
 
2996
3189
  def replace(node, new_node)
3190
+ drop_columnar_children(node)
3191
+ drop_columnar_children(new_node)
2997
3192
  if NATIVE_READ_LAYER
2998
3193
  node = to_binding(node)
2999
3194
  new_node = to_binding(new_node)
@@ -3050,6 +3245,7 @@ module Moxml
3050
3245
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
3051
3246
 
3052
3247
  def replace_children(node, new_children)
3248
+ drop_columnar_children(node)
3053
3249
  # Cross-pool children: the binding's bulk face adopts per
3054
3250
  # node and fails on mixed kinds (inner_xml= with
3055
3251
  # text+elements died with Memory allocation failed —
@@ -3129,7 +3325,33 @@ module Moxml
3129
3325
  end.join
3130
3326
  end
3131
3327
 
3328
+ # SET_TEXT_NATIVE: bindings with the unshadowed C alias
3329
+ # (the add_child_native pattern — set_text_native aliases
3330
+ # content= on the TypedData self) take the one-frame write:
3331
+ # no doc_for climb, no address read, no module dispatch.
3332
+ # Self-gating by respond_to? — older exts fall through to
3333
+ # the set_binding_text face.
3334
+ SET_TEXT_NATIVE = defined?(::Leptris::XML::NativeNode) &&
3335
+ ::Leptris::XML::NativeNode.method_defined?(:set_text_native)
3336
+
3132
3337
  def set_text_content(node, content)
3338
+ drop_columnar_children(node)
3339
+ if SET_TEXT_NATIVE && node.is_a?(::Leptris::XML::NativeNode)
3340
+ begin
3341
+ node.set_text_native(content.to_s)
3342
+ return
3343
+ rescue ::Leptris::XML::Error => e
3344
+ # Same #401 contract as the fallback below: a detached
3345
+ # (parentless) node re-raises as the named Moxml::Error.
3346
+ raise if node.parent
3347
+
3348
+ raise Moxml::Error,
3349
+ "cannot set text content on a detached " \
3350
+ "(parentless) node — attach it to a document " \
3351
+ "first (issue #401): #{e.message}"
3352
+ end
3353
+ end
3354
+
3133
3355
  # Native-layer fast path (leptris-ruby#460): the C setter
3134
3356
  # takes document+address directly — no binding twin mint
3135
3357
  # per write. The Identity wrapper's own content= would
@@ -3411,6 +3633,22 @@ module Moxml
3411
3633
  )
3412
3634
  end
3413
3635
 
3636
+ # leptris-ruby#529 bulk face: the whole nodeset as an Array
3637
+ # of registered natives in ONE crossing — the eager lane
3638
+ # below crosses once per row (the scoreboard's iterate-
3639
+ # every-result row). Real rows mint through the registered
3640
+ # klasses with native-cache identity; attr/sequence rows
3641
+ # arrive as the ResultAttr/ResultText wrappers the eager
3642
+ # mint produces, so NodeSet's per-row wrap sees one shape
3643
+ # and the result contract is unchanged. The face CONSUMES
3644
+ # the pointer for nodesets; a non-nodeset result answers
3645
+ # nil and leaves the pointer — fall through to the scalar
3646
+ # wrap, whose free owns it.
3647
+ if BULK_XPATH_ROWS
3648
+ rows = document.xpath_result_rows_bulk(result_ptr)
3649
+ return rows unless rows.nil?
3650
+ end
3651
+
3414
3652
  # The binding's #to_a mints one Ruby wrapper per result
3415
3653
  # node; hand the native set through so .size/.first stay
3416
3654
  # native-side (LazyNodeSet holds it unmaterialized).
@@ -109,6 +109,10 @@ module Moxml
109
109
  # over-invalidate (global bump instead of local) — a no-op
110
110
  # for correctness.
111
111
  if name.start_with?("xmlns")
112
+ # Introducing (or rewriting) a namespace declaration: mark the
113
+ # context BEFORE the invalidation, so this write's generation
114
+ # bump fans out to every wrapper's memoized scope reads.
115
+ element.context.mark_namespaces_active!
112
116
  adapter.set_attribute(element.native, name, value)
113
117
  element.invalidate_attribute_cache!
114
118
  return value
data/lib/moxml/context.rb CHANGED
@@ -90,7 +90,10 @@ module Moxml
90
90
  end
91
91
 
92
92
  def bump_children_generation
93
- @children_generation = children_generation + 1
93
+ # One dispatch, not two: children_generation re-dispatched just
94
+ # to default nil. Shared counter — node wrappers' children memos
95
+ # key on it.
96
+ @children_generation = @children_generation.to_i + 1
94
97
  end
95
98
 
96
99
  def namespace_scope_generation
@@ -101,6 +104,22 @@ module Moxml
101
104
  @namespace_scope_generation += 1
102
105
  end
103
106
 
107
+ # Namespace scopes only need the per-mutation generation bump
108
+ # while namespaces can exist. A builder-only document (no parse,
109
+ # no xmlns write) never has one, and the bump sat on the attach
110
+ # hot path (deletion experiment: build row 2.128x -> 1.923x).
111
+ # Every namespace introduction route marks the context active
112
+ # first — parse faces and the xmlns attribute-write face — so
113
+ # the sticky flag never misses an introduction.
114
+ def namespace_scopes_active?
115
+ @namespace_scopes_active = false if @namespace_scopes_active.nil?
116
+ @namespace_scopes_active
117
+ end
118
+
119
+ def mark_namespaces_active!
120
+ @namespace_scopes_active = true
121
+ end
122
+
104
123
  def entity_registry
105
124
  @entity_registry ||= build_entity_registry
106
125
  end
@@ -128,6 +147,7 @@ module Moxml
128
147
  # Parse with adapter, passing self (context) so adapter can use our config
129
148
  parsed_options = default_options.merge(options)
130
149
  doc = config.adapter.parse(xml_string, parsed_options, self)
150
+ mark_namespaces_active!
131
151
 
132
152
  # Set declaration flag on Document wrapper (proper OOP)
133
153
  doc.has_xml_declaration = has_declaration if doc.is_a?(Document)
@@ -233,7 +253,9 @@ module Moxml
233
253
  # are the result either way.
234
254
  def parse_fragment(xml)
235
255
  adapter = config.adapter
236
- adapter.parse_fragment(xml, self).map do |node|
256
+ nodes = adapter.parse_fragment(xml, self)
257
+ mark_namespaces_active!
258
+ nodes.map do |node|
237
259
  # Adapters that tie a parent_node lifetime chain (#245)
238
260
  # return wrapped nodes — pass them through untouched;
239
261
  # re-wrapping would race the weak wrapper map.
@@ -242,6 +264,7 @@ module Moxml
242
264
  end
243
265
 
244
266
  def iterparse(xml, mode: :top_level, &block)
267
+ mark_namespaces_active!
245
268
  config.adapter.iterparse(xml, mode, self, &block)
246
269
  end
247
270
 
@@ -163,12 +163,26 @@ module Moxml
163
163
  end
164
164
 
165
165
  def create_element(name)
166
- native = adapter.create_element(name, owner_doc: @native)
166
+ # The mint path dereferenced `adapter` three times and re-probed
167
+ # `mints_primed?` twice per element; the probe reads a class
168
+ # constant, so its verdict is fixed for the context's lifetime —
169
+ # deref once into a local and memoize the probe (the accessor's
170
+ # context hop ran on every mint otherwise).
171
+ a = @adapter || (@adapter = context.config.adapter)
172
+ primed = @mints_primed
173
+ primed = (@mints_primed = a.mints_primed?) if primed.nil?
174
+ native = if primed
175
+ a.create_primed_element(name, owner_doc: @native,
176
+ context: context, adapter: a)
177
+ else
178
+ a.create_element(name, owner_doc: @native)
179
+ end
167
180
  # leptris-ruby#508 mints registered klasses at create: the node
168
181
  # IS a contract-carrying identity (@native is self) — prime it
169
182
  # in place instead of shelling a second wrapper (a raw mint
170
183
  # reaching ==/adapter dies on the nil @context).
171
- return native.prime_contract(native, context, adapter) if native.is_a?(Moxml::Node)
184
+ return native if primed && native.is_a?(Moxml::Node)
185
+ return native.prime_contract(native, context, a) if native.is_a?(Moxml::Node)
172
186
 
173
187
  Wrappers::Element.new(native, context)
174
188
  end
data/lib/moxml/element.rb CHANGED
@@ -276,6 +276,7 @@ module Moxml
276
276
  def add_namespace(prefix, uri)
277
277
  adapter.create_namespace(@native, prefix, uri,
278
278
  namespace_validation_mode: context.config.namespace_validation_mode)
279
+ context.mark_namespaces_active!
279
280
  invalidate_namespace_cache!
280
281
  self
281
282
  rescue ValidationError => e
@@ -530,8 +531,19 @@ module Moxml
530
531
 
531
532
  # Clear the namespace caches and bump the context's scope
532
533
  # generation so every wrapper — including ones not reachable from
533
- # any children cache — recomputes on next read.
534
+ # any children cache — recomputes on next read. While namespaces
535
+ # cannot exist (context.namespace_scopes_active? is false) the
536
+ # memos can only hold namespace-free reads, so skipping the whole
537
+ # body is exact — and the nil-assignment form would MATERIALIZE
538
+ # three unset ivars per attached node, which is the cost this
539
+ # gate exists to remove. Every introduction route — parse faces
540
+ # and the xmlns write face — marks the context BEFORE its own
541
+ # invalidation, so the introducing mutation still clears and
542
+ # bumps; in-scope reads are generation-keyed, so other wrappers
543
+ # recompute on the bump.
534
544
  def invalidate_namespace_cache!
545
+ return unless context.namespace_scopes_active?
546
+
535
547
  @namespaces = nil
536
548
  @namespace_definitions = nil
537
549
  @in_scope_namespaces = nil
data/lib/moxml/node.rb CHANGED
@@ -30,9 +30,11 @@ module Moxml
30
30
  def prime_contract(native, context, adapter = nil, node_type = nil)
31
31
  @context = context
32
32
  @native = native
33
- @parent_node = nil
34
- @adapter = adapter
35
- @node_type_cached = node_type
33
+ # @parent_node and @node_type_cached stay UNSET when empty —
34
+ # every reader is nil-tolerant, and a nil write materializes the
35
+ # ivar (object-shape transition) on every created node.
36
+ @adapter = adapter unless adapter.nil?
37
+ @node_type_cached = node_type unless node_type.nil?
36
38
  self
37
39
  end
38
40
 
@@ -213,15 +215,42 @@ module Moxml
213
215
 
214
216
  def attach_child(node)
215
217
  context.bump_children_generation
216
- node = prepare_node(node)
217
- adapter.add_child(@native, node.native)
218
+ # Inlined prepare_node: the per-attach dispatch frame measured
219
+ # ~4.4% on the builder hot path (deletion experiment). Same
220
+ # contract — String operands mint literal text nodes, foreign
221
+ # types raise.
222
+ node = case node
223
+ when Node then node
224
+ when String then Moxml::Node.wrap(adapter.create_text(node), context)
225
+ else
226
+ raise Moxml::DocumentStructureError.new(
227
+ "Invalid node type: #{node.class}. Expected String or Moxml::Node",
228
+ operation: "attach_child",
229
+ state: "node_type: #{node.class}",
230
+ )
231
+ end
232
+ oc = node.context
233
+ if !oc.nil? && !oc.equal?(context) &&
234
+ oc.namespace_scopes_active?
235
+ context.mark_namespaces_active!
236
+ end
237
+ a = adapter
238
+ a.add_child(@native, node.native)
218
239
  # Refresh native in case adapter changed identity (e.g., LibXML
219
240
  # doc.root=); stable-identity adapters skip the round trip.
220
- unless adapter.native_identity_stable?
241
+ unless a.native_identity_stable?
221
242
  refreshed = adapter.actual_native(node.native, @native)
222
243
  node.refresh_native!(refreshed) if refreshed && refreshed != node.native
223
244
  end
224
- node.parent_node = self
245
+ # The wrapper-side parent link rides the adapter's capability
246
+ # gate: adapters whose natives cannot resolve a parent through
247
+ # the adapter layer need it for remove/replace to find the tree
248
+ # (ox's raw-text natives, for one); identity-stable adapters
249
+ # skip the write, which otherwise materializes a fourth ivar on
250
+ # every attached node (object-shape transition per attach).
251
+ # Wrappers that need the link anyway get it where they are
252
+ # minted with one — NodeSet#each re-points them.
253
+ node.parent_node = self if a.attach_sets_parent_link?
225
254
  # The adopted subtree's in-scope namespaces changed
226
255
  node.invalidate_namespace_cache!
227
256
  invalidate_children_cache!
@@ -231,6 +260,7 @@ module Moxml
231
260
  def attach_previous_sibling(node)
232
261
  context.bump_children_generation
233
262
  node = prepare_node(node)
263
+ absorb_namespace_activity(node)
234
264
  inserted = adapter.add_previous_sibling(@native, node.native)
235
265
  refresh_inserted_native(node, inserted)
236
266
  # Invalidate the parent's memoized children list. The wrapper-side
@@ -245,6 +275,7 @@ module Moxml
245
275
  def attach_next_sibling_to(anchor, node)
246
276
  context.bump_children_generation
247
277
  node = prepare_node(node)
278
+ absorb_namespace_activity(node)
248
279
  inserted = adapter.add_next_sibling(anchor.native, node.native)
249
280
  refresh_inserted_native(node, inserted)
250
281
  anchor.parent&.invalidate_children_cache!
@@ -252,6 +283,16 @@ module Moxml
252
283
  self
253
284
  end
254
285
 
286
+ # A node adopted from ANOTHER context can carry namespaces this
287
+ # context's own faces (parse, xmlns writes) never saw; import the
288
+ # source context's activity flag so the gated scope-generation
289
+ # bump stays armed here too.
290
+ def absorb_namespace_activity(node)
291
+ oc = node.context
292
+ return if oc.nil? || oc.equal?(context)
293
+ context.mark_namespaces_active! if oc.namespace_scopes_active?
294
+ end
295
+
255
296
  def refresh_inserted_native(node, inserted)
256
297
  return unless adapter.add_sibling_returns_inserted?
257
298
  return if inserted.nil? || inserted.equal?(node.native)
@@ -773,7 +814,10 @@ module Moxml
773
814
  protected
774
815
 
775
816
  def invalidate_children_cache!
776
- @children = nil
817
+ # Only nil an EXISTING memo: nil-assignment materializes an
818
+ # unset ivar (object-shape transition), and the builder hot
819
+ # path invalidates parents whose children memo was never read.
820
+ @children = nil unless @children.nil?
777
821
  end
778
822
 
779
823
  # Invalidate parent's cached children when this node
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.150"
4
+ VERSION = "0.5.152"
5
5
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "leptris"
5
+ rescue LoadError
6
+ # Leptris gem not available - skip all specs in this file
7
+ return
8
+ end
9
+ return unless Leptris::XML::Document.respond_to?(:create)
10
+ # The columnar children lane rides the binding's walk_columnar face
11
+ # (leptris-ruby#527/#532); older bindings keep the per-node children
12
+ # crossing, so the routing cannot be pinned there.
13
+ return unless defined?(Leptris::XML::Native) &&
14
+ Leptris::XML::Native.respond_to?(:walk_columnar)
15
+
16
+ require "moxml/adapter/leptris"
17
+
18
+ RSpec.describe "Moxml::Adapter::Leptris columnar children (SOTA B2 walk row)" do
19
+ around do |example|
20
+ Moxml.with_config(:leptris, true, "UTF-8") do
21
+ example.run
22
+ end
23
+ end
24
+
25
+ let(:xml) do
26
+ <<~XML
27
+ <?xml version="1.0"?><r><a x="1">t</a><b/><c><d/></c><tail>z</tail></r>
28
+ XML
29
+ end
30
+
31
+ let(:doc) { Moxml.parse(xml) }
32
+ let(:root) { doc.root }
33
+ let(:root_native) { root.native }
34
+
35
+ # Counts real Native.walk_columnar C crossings inside the block —
36
+ # no stubs, the actual boundary traffic the walk row pays.
37
+ def with_crossing_count
38
+ calls = 0
39
+ tp = TracePoint.new(:c_call) do |t|
40
+ calls += 1 if t.method_id == :walk_columnar
41
+ end
42
+ result = nil
43
+ tp.enable { result = yield }
44
+ [calls, result]
45
+ end
46
+
47
+ describe "the columnar gate" do
48
+ it "face-probes walk_columnar on the binding" do
49
+ expect(Moxml::Adapter::Leptris::COLUMNAR_WALK).to be(true)
50
+ expect(Leptris::XML::Native.respond_to?(:walk_columnar)).to be(true)
51
+ end
52
+ end
53
+
54
+ describe "plain children reads" do
55
+ it "answer natives identical to the native children face" do
56
+ got = Moxml::Adapter::Leptris.children(root_native)
57
+ expected = Leptris::XML::NativeNode
58
+ .instance_method(:children).bind_call(root_native).to_a
59
+ expect(got.size).to eq(expected.size)
60
+ expect(got.map { |n| Moxml::Adapter::Leptris.node_name(n) })
61
+ .to eq(expected.map(&:name))
62
+ got.each_with_index do |native, i|
63
+ # native-cache identity: cached handles ARE the children_read
64
+ # wrappers, so the NodeSet contract holds.
65
+ expect(native).to equal(expected[i])
66
+ end
67
+ end
68
+
69
+ # Parsed-document wrappers mint with Identity::ChildReads above
70
+ # Moxml::Node, so consumer iteration on those wrappers dispatches
71
+ # to the binding's C read layer. The children FACE is pinned by
72
+ # driving it for every node of the cold subtree: one walk_columnar
73
+ # crossing primes the whole map, every deeper face read hits it.
74
+ it "pay ONE columnar crossing for a whole cold subtree" do
75
+ calls, = with_crossing_count do
76
+ walk_face = lambda do |native|
77
+ Moxml::Adapter::Leptris.children(native).each do |child|
78
+ walk_face.call(child)
79
+ end
80
+ end
81
+ walk_face.call(root_native)
82
+ end
83
+ expect(calls).to eq(1)
84
+ end
85
+
86
+ it "serve repeat face reads from the primed map without a crossing" do
87
+ calls, first = with_crossing_count do
88
+ Moxml::Adapter::Leptris.children(root_native)
89
+ end
90
+ expect(calls).to eq(1)
91
+ first_names = first.map { |n| Moxml::Adapter::Leptris.node_name(n) }
92
+ calls2, second = with_crossing_count do
93
+ Moxml::Adapter::Leptris.children(root_native)
94
+ end
95
+ expect(calls2).to eq(0)
96
+ expect(second.map { |n| Moxml::Adapter::Leptris.node_name(n) })
97
+ .to eq(first_names)
98
+ end
99
+ end
100
+
101
+ describe "mutation invalidation" do
102
+ # Read back through the adapter face: the map must never serve a
103
+ # list a mutation has invalidated.
104
+ def face_children_names
105
+ Moxml::Adapter::Leptris.children(root_native)
106
+ .map { |n| Moxml::Adapter::Leptris.node_name(n) }
107
+ end
108
+
109
+ it "sees children added after the map was primed" do
110
+ face_children_names # prime the map
111
+ root.add_child(doc.create_element("late"))
112
+ expect(face_children_names).to include("late")
113
+ end
114
+
115
+ it "sees children removed after the map was primed" do
116
+ face_children_names # prime the map
117
+ root.children.to_a.first.remove
118
+ expect(face_children_names).not_to include("a")
119
+ end
120
+
121
+ it "sees sibling insertions after the map was primed" do
122
+ face_children_names # prime the map
123
+ anchor = root.children.to_a.first
124
+ anchor.add_previous_sibling(doc.create_element("before2"))
125
+ expect(face_children_names).to include("before2")
126
+ end
127
+
128
+ it "sees replace after the map was primed" do
129
+ face_children_names # prime the map
130
+ victim = root.children.to_a.find { |n| n.name == "b" }
131
+ victim.replace(doc.create_element("swapped"))
132
+ expect(face_children_names).to include("swapped")
133
+ expect(face_children_names).not_to include("b")
134
+ end
135
+
136
+ it "sees content rewrites after the map was primed" do
137
+ face_children_names # prime the map
138
+ target = root.children.to_a.find { |n| n.name == "a" }
139
+ target.content = "rewritten"
140
+ names = face_children_names
141
+ expect(names).to include("a")
142
+ expect(root.children.to_a.find { |n| n.name == "a" }.text)
143
+ .to eq("rewritten")
144
+ end
145
+ end
146
+ end
@@ -865,5 +865,95 @@ RSpec.describe Moxml::Adapter::Leptris do
865
865
  expect(native).not_to be_nil
866
866
  expect(native.to_a.map { |n| n.name.to_s }).to eq(%w[a c b])
867
867
  end
868
+
869
+ it "never arms the guard on engines that invalidate on splice" do
870
+ skip "requires an engine whose node-set index survives sibling insertion (leptris-ruby#519)" unless described_class.native_xpath_index_fixed?
871
+
872
+ doc = ctx.parse("<root><anchor/></root>")
873
+ doc.xpath("//ext") # prime the engine index
874
+ doc.root.add_child(doc.create_element("ext"))
875
+ expect(doc.xpath("//ext").size).to eq(1)
876
+
877
+ expect(
878
+ described_class.attachments.get(
879
+ described_class.xpath_index_document(doc.root.native),
880
+ described_class.singleton_class.const_get(:XPATH_INDEX_STALE),
881
+ ),
882
+ ).to be_nil
883
+ end
884
+ end
885
+
886
+ describe "namespace scope-generation gate" do
887
+ let(:ctx) { Moxml.new(:leptris) }
888
+
889
+ # The per-attach scope-generation bump only matters once
890
+ # namespaces exist; the builder hot path must not pay it.
891
+ it "does not bump the scope generation while namespaces are absent" do
892
+ doc = ctx.create_document
893
+ root = doc.create_element("r")
894
+ doc.root = root
895
+ gen = ctx.namespace_scope_generation
896
+ root.add_child(doc.create_element("a"))
897
+ root.add_child(doc.create_element("b"))
898
+ expect(ctx.namespace_scope_generation).to eq(gen)
899
+ expect(ctx.namespace_scopes_active?).to be(false)
900
+ end
901
+
902
+ it "arms the gate when a write introduces a namespace" do
903
+ doc = ctx.create_document
904
+ root = doc.create_element("r")
905
+ doc.root = root
906
+ root["xmlns:foo"] = "urn:x"
907
+ expect(ctx.namespace_scopes_active?).to be(true)
908
+ expect(root.namespaces.map { |n| [n.prefix, n.uri] })
909
+ .to include(%w[foo urn:x])
910
+ end
911
+
912
+ it "is armed by parsed documents" do
913
+ doc = ctx.parse("<r xmlns:foo='urn:x'><foo:a/></r>")
914
+ expect(ctx.namespace_scopes_active?).to be(true)
915
+ expect(doc.xpath("//foo:a", "foo" => "urn:x").size).to eq(1)
916
+ end
917
+
918
+ it "imports the flag from a foreign context on adoption" do
919
+ donor_ctx = Moxml.new(:leptris)
920
+ donor = donor_ctx.parse("<x xmlns:foo='urn:x'/>")
921
+ doc = ctx.create_document
922
+ root = doc.create_element("r")
923
+ doc.root = root
924
+ expect(ctx.namespace_scopes_active?).to be(false)
925
+ root.add_child(donor.root)
926
+ expect(ctx.namespace_scopes_active?).to be(true)
927
+ expect(root.children.first.namespaces.map(&:uri))
928
+ .to include("urn:x")
929
+ end
930
+ end
931
+
932
+ # The README error-handling contract: an invalid element name must
933
+ # raise Moxml::ValidationError at CREATE time. The adapter's native
934
+ # fast paths (create_primed_element, create_element_with_attrs) mint
935
+ # from a name without passing through Base#create_element, where
936
+ # validate_element_name lives — the failure then surfaced late, at
937
+ # add_child, as Moxml::Error "Document already has a root element".
938
+ describe "element name validation on the create seam" do
939
+ let(:ctx) { Moxml.new(:leptris) }
940
+
941
+ it "raises ValidationError when create_element mints an invalid name" do
942
+ doc = ctx.parse("<root/>")
943
+ expect { doc.create_element("wrong,name") }
944
+ .to raise_error(Moxml::ValidationError, /Invalid XML element name/)
945
+ end
946
+
947
+ it "raises ValidationError when add_element mints an invalid name" do
948
+ doc = ctx.create_document
949
+ expect { doc.add_element("bad,name", id: "1") }
950
+ .to raise_error(Moxml::ValidationError, /Invalid XML element name/)
951
+ end
952
+
953
+ it "still mints valid names with punctuation allowed in XML Names" do
954
+ doc = ctx.parse("<root/>")
955
+ element = doc.create_element("ns:my-element.v2")
956
+ expect(element).to be_a(Moxml::Element)
957
+ end
868
958
  end
869
959
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "leptris"
5
+ rescue LoadError
6
+ # Leptris gem not available - skip all specs in this file
7
+ return
8
+ end
9
+
10
+ require "moxml/adapter/leptris"
11
+
12
+ # leptris-ruby#529: the multi-result native XPath lane routes through
13
+ # Document#xpath_result_rows_bulk when the binding answers it — the
14
+ # whole node-set in ONE C crossing instead of one crossing per row.
15
+ # The rows keep the eager lane's contract (registered wrapper
16
+ # klasses, ResultAttr/ResultText synthetic rows, scalar pass-through
17
+ # on non-node-set results), so every consumer-visible shape is
18
+ # unchanged; what pins here is the routing gate and those shapes.
19
+ RSpec.describe "leptris xpath bulk rows routing" do
20
+ let(:ctx) { Moxml.new(:leptris) }
21
+ let(:doc) do
22
+ ctx.parse(<<~XML)
23
+ <root>
24
+ <item id="1">alpha</item>
25
+ <item id="2">beta</item>
26
+ <!-- note -->
27
+ </root>
28
+ XML
29
+ end
30
+
31
+ it "arms the gate on the current binding" do
32
+ expect(Leptris::XML::Native.respond_to?(:xpath_result_rows_bulk)).to be(true)
33
+ expect(Leptris::XML::Document.method_defined?(:xpath_result_rows_bulk)).to be(true)
34
+ expect(Moxml::Adapter::Leptris::BULK_XPATH_ROWS).to be(true)
35
+ end
36
+
37
+ it "answers multi-result queries with wrapped natives in order" do
38
+ set = doc.xpath("//item")
39
+ expect(set).to be_a(Moxml::NodeSet)
40
+ expect(set.size).to eq(2)
41
+ expect(set.map(&:name)).to eq(%w[item item])
42
+ expect(set.map(&:text)).to eq(%w[alpha beta])
43
+ expect(set.first).to be_a(Moxml::Element)
44
+ end
45
+
46
+ it "keeps attribute rows as Attribute wrappers" do
47
+ attrs = doc.xpath("//item/@id")
48
+ expect(attrs.map(&:name)).to eq(%w[id id])
49
+ expect(attrs.map(&:value)).to eq(%w[1 2])
50
+ expect(attrs.first).to be_a(Moxml::Attribute)
51
+ end
52
+
53
+ it "keeps mixed element and comment rows in document order" do
54
+ set = doc.xpath("/root/node()").grep_v(Moxml::Text)
55
+ expect(set.map { |n| n.is_a?(Moxml::Comment) ? :comment : n.name.to_sym })
56
+ .to eq(%i[item item comment])
57
+ end
58
+
59
+ it "passes scalars through unwrapped" do
60
+ expect(doc.xpath("count(//item)")).to eq(2.0)
61
+ expect(doc.xpath("string(//item)")).to eq("alpha")
62
+ expect(doc.xpath("//item[@id='9']")).to be_a(Moxml::NodeSet)
63
+ expect(doc.xpath("//item[@id='9']").empty?).to be(true)
64
+ end
65
+
66
+ it "resolves one address to one Ruby object across queries" do
67
+ first = doc.xpath("//item").first
68
+ again = doc.xpath("//item").first
69
+ expect(again).to be(first)
70
+ end
71
+ 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.150
4
+ version: 0.5.152
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-21 00:00:00.000000000 Z
11
+ date: 2026-10-22 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
@@ -481,6 +481,7 @@ files:
481
481
  - spec/moxml/adapter/leptris_append_gc_spec.rb
482
482
  - spec/moxml/adapter/leptris_attribute_snapshot_spec.rb
483
483
  - spec/moxml/adapter/leptris_binding_cap_spec.rb
484
+ - spec/moxml/adapter/leptris_children_columnar_spec.rb
484
485
  - spec/moxml/adapter/leptris_contract_reads_spec.rb
485
486
  - spec/moxml/adapter/leptris_detached_text_spec.rb
486
487
  - spec/moxml/adapter/leptris_doc_parts_spec.rb
@@ -493,6 +494,7 @@ files:
493
494
  - spec/moxml/adapter/leptris_sax_records_spec.rb
494
495
  - spec/moxml/adapter/leptris_spec.rb
495
496
  - spec/moxml/adapter/leptris_strict_mismatch_spec.rb
497
+ - spec/moxml/adapter/leptris_xpath_bulk_rows_spec.rb
496
498
  - spec/moxml/adapter/leptris_xpath_identity_spec.rb
497
499
  - spec/moxml/adapter/libxml_internals_spec.rb
498
500
  - spec/moxml/adapter/libxml_spec.rb