moxml 0.5.151 → 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/lib/moxml/adapter/base.rb +9 -0
- data/lib/moxml/adapter/leptris.rb +184 -1
- data/lib/moxml/document.rb +14 -10
- data/lib/moxml/node.rb +23 -2
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_children_columnar_spec.rb +146 -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/lib/moxml/adapter/base.rb
CHANGED
|
@@ -595,6 +595,15 @@ namespace_validation_mode: :strict)
|
|
|
595
595
|
false
|
|
596
596
|
end
|
|
597
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
|
+
|
|
598
607
|
# Whether a BARE-name attribute READ addresses only the
|
|
599
608
|
# no-namespace attribute (qualified-name semantics) — the
|
|
600
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).
|
|
@@ -1203,6 +1230,9 @@ module Moxml
|
|
|
1203
1230
|
# itself out, no moxml release needed.
|
|
1204
1231
|
XPATH_INDEX_STALE = :xpath_index_stale
|
|
1205
1232
|
|
|
1233
|
+
# Attachment key for the columnar children map (COLUMNAR_WALK).
|
|
1234
|
+
COLUMNAR_CHILDREN = :columnar_children_map
|
|
1235
|
+
|
|
1206
1236
|
def mark_xpath_index_stale(node)
|
|
1207
1237
|
return if native_xpath_index_fixed?
|
|
1208
1238
|
|
|
@@ -1253,6 +1283,8 @@ module Moxml
|
|
|
1253
1283
|
end
|
|
1254
1284
|
|
|
1255
1285
|
def set_root(doc, element)
|
|
1286
|
+
drop_columnar_children(doc)
|
|
1287
|
+
drop_columnar_children(element)
|
|
1256
1288
|
to_binding(doc).root = to_binding(element)
|
|
1257
1289
|
end
|
|
1258
1290
|
|
|
@@ -1331,6 +1363,19 @@ module Moxml
|
|
|
1331
1363
|
true
|
|
1332
1364
|
end
|
|
1333
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
|
+
|
|
1334
1379
|
# Our at_xpath falls back to the model's Ruby engine when the
|
|
1335
1380
|
# native lane answers nothing — the engine's node-set XPath
|
|
1336
1381
|
# index is stale after sibling insertion, so `at` must not be
|
|
@@ -1639,6 +1684,7 @@ module Moxml
|
|
|
1639
1684
|
# re-indented). Raw-pointer walk over the root subtree — the
|
|
1640
1685
|
# same batch child-pointer pattern the materializer uses.
|
|
1641
1686
|
def strip_blank_text_nodes!(doc)
|
|
1687
|
+
drop_columnar_children(doc)
|
|
1642
1688
|
binding_ffi = ::Leptris::XML::FFI
|
|
1643
1689
|
root = binding_ffi.leptris_document_root(doc.c_ptr)
|
|
1644
1690
|
return unless root && !root.null?
|
|
@@ -1788,7 +1834,11 @@ module Moxml
|
|
|
1788
1834
|
|
|
1789
1835
|
def create_primed_element(name, owner_doc:, context:, adapter:)
|
|
1790
1836
|
validate_element_name(name)
|
|
1791
|
-
|
|
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)
|
|
1792
1842
|
end
|
|
1793
1843
|
|
|
1794
1844
|
def create_native_text(content, owner_doc = nil)
|
|
@@ -2132,6 +2182,8 @@ module Moxml
|
|
|
2132
2182
|
# source. Answer the installed binding element; callers re-point
|
|
2133
2183
|
# wrappers at it.
|
|
2134
2184
|
def adopt_root(doc, element)
|
|
2185
|
+
drop_columnar_children(doc)
|
|
2186
|
+
drop_columnar_children(element)
|
|
2135
2187
|
return nil unless NATIVE_ROOT_ADOPT
|
|
2136
2188
|
|
|
2137
2189
|
binding_doc = to_binding(doc)
|
|
@@ -2170,6 +2222,18 @@ module Moxml
|
|
|
2170
2222
|
# TextSegment reconstruction reads binding text nodes;
|
|
2171
2223
|
# the wrapper's memo supplies the flag, so deriving it
|
|
2172
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
|
+
|
|
2173
2237
|
natives = if NATIVE_BULK_FIXED
|
|
2174
2238
|
NN_CHILDREN.bind_call(node).to_a
|
|
2175
2239
|
else
|
|
@@ -2224,6 +2288,80 @@ module Moxml
|
|
|
2224
2288
|
end
|
|
2225
2289
|
end
|
|
2226
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
|
+
|
|
2227
2365
|
def native_keep_entity_refs?(node)
|
|
2228
2366
|
return false unless NATIVE_READ_LAYER &&
|
|
2229
2367
|
node.is_a?(::Leptris::XML::NativeNode)
|
|
@@ -2437,6 +2575,7 @@ module Moxml
|
|
|
2437
2575
|
# callers fall back to create_element + set_attribute +
|
|
2438
2576
|
# add_child when absent.
|
|
2439
2577
|
def create_element_with_attrs(parent, name, attrs)
|
|
2578
|
+
drop_columnar_children(parent)
|
|
2440
2579
|
validate_element_name(name)
|
|
2441
2580
|
binding_parent = to_binding(parent)
|
|
2442
2581
|
doc = binding_parent.document or return nil
|
|
@@ -2517,7 +2656,22 @@ module Moxml
|
|
|
2517
2656
|
attr.name.to_s.dup.force_encoding("UTF-8")
|
|
2518
2657
|
end
|
|
2519
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
|
+
|
|
2520
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
|
+
|
|
2521
2675
|
element = to_binding(element) if NATIVE_READ_LAYER
|
|
2522
2676
|
element[name.to_s] = value.to_s
|
|
2523
2677
|
end
|
|
@@ -2728,6 +2882,8 @@ module Moxml
|
|
|
2728
2882
|
# on the source, so a living source can never outlive the
|
|
2729
2883
|
# memory its reads still touch.
|
|
2730
2884
|
def absorb_source_document(dest, owner)
|
|
2885
|
+
drop_columnar_children(dest)
|
|
2886
|
+
drop_columnar_children(owner)
|
|
2731
2887
|
return nil unless NATIVE_DOC_ABSORB
|
|
2732
2888
|
return :direct if attachments.get(dest, :absorbed_sources)&.include?(owner)
|
|
2733
2889
|
|
|
@@ -2773,6 +2929,8 @@ module Moxml
|
|
|
2773
2929
|
end
|
|
2774
2930
|
|
|
2775
2931
|
def add_child(parent, child)
|
|
2932
|
+
drop_columnar_children(parent)
|
|
2933
|
+
drop_columnar_children(child)
|
|
2776
2934
|
result = add_child_impl(parent, child)
|
|
2777
2935
|
# A successful child insertion rebuilds the engine's
|
|
2778
2936
|
# document index; the upstream sibling-insert bug leaves it
|
|
@@ -2894,6 +3052,8 @@ module Moxml
|
|
|
2894
3052
|
end
|
|
2895
3053
|
|
|
2896
3054
|
def add_previous_sibling(node, new_node)
|
|
3055
|
+
drop_columnar_children(node)
|
|
3056
|
+
drop_columnar_children(new_node)
|
|
2897
3057
|
result = add_previous_sibling_impl(node, new_node)
|
|
2898
3058
|
mark_xpath_index_stale(node)
|
|
2899
3059
|
result
|
|
@@ -2940,6 +3100,8 @@ module Moxml
|
|
|
2940
3100
|
end
|
|
2941
3101
|
|
|
2942
3102
|
def add_next_sibling(node, new_node)
|
|
3103
|
+
drop_columnar_children(node)
|
|
3104
|
+
drop_columnar_children(new_node)
|
|
2943
3105
|
result = add_next_sibling_impl(node, new_node)
|
|
2944
3106
|
mark_xpath_index_stale(node)
|
|
2945
3107
|
result
|
|
@@ -2993,6 +3155,7 @@ module Moxml
|
|
|
2993
3155
|
end
|
|
2994
3156
|
|
|
2995
3157
|
def remove(node)
|
|
3158
|
+
drop_columnar_children(node)
|
|
2996
3159
|
node = to_binding(node) if NATIVE_READ_LAYER
|
|
2997
3160
|
case node
|
|
2998
3161
|
when CustomizedLeptris::Declaration
|
|
@@ -3024,6 +3187,8 @@ module Moxml
|
|
|
3024
3187
|
end
|
|
3025
3188
|
|
|
3026
3189
|
def replace(node, new_node)
|
|
3190
|
+
drop_columnar_children(node)
|
|
3191
|
+
drop_columnar_children(new_node)
|
|
3027
3192
|
if NATIVE_READ_LAYER
|
|
3028
3193
|
node = to_binding(node)
|
|
3029
3194
|
new_node = to_binding(new_node)
|
|
@@ -3080,6 +3245,7 @@ module Moxml
|
|
|
3080
3245
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
|
|
3081
3246
|
|
|
3082
3247
|
def replace_children(node, new_children)
|
|
3248
|
+
drop_columnar_children(node)
|
|
3083
3249
|
# Cross-pool children: the binding's bulk face adopts per
|
|
3084
3250
|
# node and fails on mixed kinds (inner_xml= with
|
|
3085
3251
|
# text+elements died with Memory allocation failed —
|
|
@@ -3169,6 +3335,7 @@ module Moxml
|
|
|
3169
3335
|
::Leptris::XML::NativeNode.method_defined?(:set_text_native)
|
|
3170
3336
|
|
|
3171
3337
|
def set_text_content(node, content)
|
|
3338
|
+
drop_columnar_children(node)
|
|
3172
3339
|
if SET_TEXT_NATIVE && node.is_a?(::Leptris::XML::NativeNode)
|
|
3173
3340
|
begin
|
|
3174
3341
|
node.set_text_native(content.to_s)
|
|
@@ -3466,6 +3633,22 @@ module Moxml
|
|
|
3466
3633
|
)
|
|
3467
3634
|
end
|
|
3468
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
|
+
|
|
3469
3652
|
# The binding's #to_a mints one Ruby wrapper per result
|
|
3470
3653
|
# node; hand the native set through so .size/.first stay
|
|
3471
3654
|
# native-side (LazyNodeSet holds it unmaterialized).
|
data/lib/moxml/document.rb
CHANGED
|
@@ -163,22 +163,26 @@ module Moxml
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def create_element(name)
|
|
166
|
-
# The
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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)
|
|
174
177
|
else
|
|
175
|
-
|
|
178
|
+
a.create_element(name, owner_doc: @native)
|
|
176
179
|
end
|
|
177
180
|
# leptris-ruby#508 mints registered klasses at create: the node
|
|
178
181
|
# IS a contract-carrying identity (@native is self) — prime it
|
|
179
182
|
# in place instead of shelling a second wrapper (a raw mint
|
|
180
183
|
# reaching ==/adapter dies on the nil @context).
|
|
181
|
-
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)
|
|
182
186
|
|
|
183
187
|
Wrappers::Element.new(native, context)
|
|
184
188
|
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -215,7 +215,20 @@ module Moxml
|
|
|
215
215
|
|
|
216
216
|
def attach_child(node)
|
|
217
217
|
context.bump_children_generation
|
|
218
|
-
|
|
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
|
|
219
232
|
oc = node.context
|
|
220
233
|
if !oc.nil? && !oc.equal?(context) &&
|
|
221
234
|
oc.namespace_scopes_active?
|
|
@@ -229,7 +242,15 @@ module Moxml
|
|
|
229
242
|
refreshed = adapter.actual_native(node.native, @native)
|
|
230
243
|
node.refresh_native!(refreshed) if refreshed && refreshed != node.native
|
|
231
244
|
end
|
|
232
|
-
|
|
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?
|
|
233
254
|
# The adopted subtree's in-scope namespaces changed
|
|
234
255
|
node.invalidate_namespace_cache!
|
|
235
256
|
invalidate_children_cache!
|
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
|
|
@@ -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
|