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 +4 -4
- data/docs/_pages/ecosystem.adoc +2 -2
- data/lib/moxml/adapter/base.rb +16 -0
- data/lib/moxml/adapter/leptris.rb +238 -0
- data/lib/moxml/attribute_resolver.rb +4 -0
- data/lib/moxml/context.rb +25 -2
- data/lib/moxml/document.rb +16 -2
- data/lib/moxml/element.rb +13 -1
- data/lib/moxml/node.rb +52 -8
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_children_columnar_spec.rb +146 -0
- data/spec/moxml/adapter/leptris_spec.rb +90 -0
- data/spec/moxml/adapter/leptris_xpath_bulk_rows_spec.rb +71 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a384caeb57d40002410d3ebfb5ef9a22eed2793caab5db918d9bfeacaacbdee
|
|
4
|
+
data.tar.gz: 166f445cae5e401f0cf5be6a3f766eb9232809fe1217e3e90e46f2e315afce8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc19f58546f479a713b3039295b2eac40b5c83166dacc77af0017e83b40f2d3f8bffb028767f744ef29dae5688c3c5d3c34a248e9d3f75bede910b444c04b675
|
|
7
|
+
data.tar.gz: 9f9a64184feb0ba0a0eba61300a014339b81c7a58d69a1b1ff0dd4610abb7c239a6184e260253542f4643410fcfe3b15e54470b72cdb72283e7bf5a85f3d1639
|
data/docs/_pages/ecosystem.adoc
CHANGED
|
@@ -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
|
|
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:)`) | **
|
|
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
|
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
data/lib/moxml/document.rb
CHANGED
|
@@ -163,12 +163,26 @@ module Moxml
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def create_element(name)
|
|
166
|
-
|
|
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
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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-
|
|
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
|