moxml 0.5.104 → 0.5.106
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 +30 -4
- data/lib/moxml/adapter/leptris.rb +17 -0
- data/lib/moxml/attribute.rb +21 -11
- data/lib/moxml/document.rb +15 -0
- data/lib/moxml/element.rb +46 -7
- data/lib/moxml/text.rb +9 -2
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/base_spec.rb +14 -0
- data/spec/moxml/at_search_spec.rb +62 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28a815d79bd0bc2f356a01f0a4f5337e111ab6bce63f7e8c2f7247efac096b4c
|
|
4
|
+
data.tar.gz: b0a75eb438464fe9327fdb0420f14be0e8b19fea4252fe8abc995e295a6c6449
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33363ebc463f8e46fbd955ad3adb8f43b01775c27cc72ddaf356e62761e9a7b1917c1c15de36716806a1c306c111368fa0705ec1e37a11fd386f6183c274f291
|
|
7
|
+
data.tar.gz: aef109c340cdcb4f30a76f87ef05e2568091e01c3dcf815fa2c945ff1cc49a961a0c36e1541f32873cca0226af2bd43a019c7cdac0c5f89b195558c2090a621a
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -131,11 +131,29 @@ module Moxml
|
|
|
131
131
|
def children_accepts_entity_flag?
|
|
132
132
|
return @children_accepts_entity_flag unless @children_accepts_entity_flag.nil?
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# Opal records parameter data only when compiling with arity
|
|
135
|
+
# checking, which is off by default (opal-rspec turns it on, so
|
|
136
|
+
# the Opal spec run never sees this). Without it
|
|
137
|
+
# Method#parameters returns a bare JS undefined, which answers no
|
|
138
|
+
# method call (not even nil?), so it is matched with `case`,
|
|
139
|
+
# whose Module#=== tolerates it. Without the data the keyword is
|
|
140
|
+
# assumed: every built-in adapter's #children accepts it.
|
|
135
141
|
@children_accepts_entity_flag =
|
|
136
|
-
method(:children).parameters
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
children_params_flag(method(:children).parameters)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The decision over a parameters VALUE, so the Opal shape (a
|
|
146
|
+
# bare JS undefined, matched by `case`) is spec-able with real
|
|
147
|
+
# values. Without parameter data the keyword is assumed: every
|
|
148
|
+
# built-in adapter's #children accepts it.
|
|
149
|
+
KEYWORD_PARAM_KINDS = %i[key keyrest keyreq].freeze
|
|
150
|
+
|
|
151
|
+
def children_params_flag(params)
|
|
152
|
+
case params
|
|
153
|
+
when Array
|
|
154
|
+
params.any? { |kind, _| KEYWORD_PARAM_KINDS.include?(kind) }
|
|
155
|
+
else true
|
|
156
|
+
end
|
|
139
157
|
end
|
|
140
158
|
|
|
141
159
|
# Protocol-level native equality; engines with more than one
|
|
@@ -157,6 +175,14 @@ module Moxml
|
|
|
157
175
|
node
|
|
158
176
|
end
|
|
159
177
|
|
|
178
|
+
# Cross-document root adoption: adapters whose engine deep-copies
|
|
179
|
+
# a foreign root into the target document natively answer the
|
|
180
|
+
# installed native here (leptris >= 1.9.292, engine #1495);
|
|
181
|
+
# nil keeps the Ruby splice.
|
|
182
|
+
def adopt_root(_doc, _element)
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
160
186
|
# Element-only children: adapters whose engine carries the
|
|
161
187
|
# face override (leptris C filter, nokogiri native); the
|
|
162
188
|
# default type-filters the native child list so non-element
|
|
@@ -1437,6 +1437,23 @@ module Moxml
|
|
|
1437
1437
|
end
|
|
1438
1438
|
end
|
|
1439
1439
|
|
|
1440
|
+
NATIVE_ROOT_ADOPT =
|
|
1441
|
+
NATIVE_READ_LAYER &&
|
|
1442
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.292.0")
|
|
1443
|
+
|
|
1444
|
+
# The engine deep-copies the foreign subtree into the target
|
|
1445
|
+
# document's pool (leptris#1495) and the binding memoizes the
|
|
1446
|
+
# INSTALLED handle — a fresh pointer distinct from the consumed
|
|
1447
|
+
# source. Answer the installed binding element; callers re-point
|
|
1448
|
+
# wrappers at it.
|
|
1449
|
+
def adopt_root(doc, element)
|
|
1450
|
+
return nil unless NATIVE_ROOT_ADOPT
|
|
1451
|
+
|
|
1452
|
+
binding_doc = to_binding(doc)
|
|
1453
|
+
binding_doc.root = to_binding(element)
|
|
1454
|
+
binding_doc.root
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1440
1457
|
# Element-only children through the engine-level filter: the
|
|
1441
1458
|
# C face mints element-kind natives directly (klass-children),
|
|
1442
1459
|
# so interleaved text/comment nodes cost nothing. Entity
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -27,19 +27,28 @@ module Moxml
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def value
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
# Engines allocate a fresh String per read (Nokogiri attr.value);
|
|
31
|
+
# hydration walks re-read the same wrappers, so the value is
|
|
32
|
+
# memoized. A held wrapper recomputes when its owner element's
|
|
33
|
+
# value generation moves (element-side writes bump it locally —
|
|
34
|
+
# a context-wide bump would evict every wrapper per bulk-build
|
|
35
|
+
# write). Parentless wrappers (xpath ResultAttrs) are immutable
|
|
36
|
+
# captures and memo unconditionally.
|
|
37
|
+
if @parent_node
|
|
38
|
+
generation = @parent_node.attribute_value_generation
|
|
39
|
+
@value = nil if @value_gen != generation
|
|
40
|
+
@value_gen = generation
|
|
41
|
+
end
|
|
42
|
+
@value ||= begin
|
|
43
|
+
val = @native.value.to_s
|
|
44
|
+
parent = @parent_node
|
|
45
|
+
if parent.nil? || parent.entity_bearing?
|
|
46
|
+
adapter.restore_entities(val)
|
|
47
|
+
else
|
|
48
|
+
val
|
|
49
|
+
end
|
|
40
50
|
end
|
|
41
51
|
end
|
|
42
|
-
|
|
43
52
|
alias content value
|
|
44
53
|
|
|
45
54
|
# Returns raw native value without entity marker restoration.
|
|
@@ -55,6 +64,7 @@ module Moxml
|
|
|
55
64
|
else
|
|
56
65
|
@parent_node&.invalidate_attribute_value_cache!
|
|
57
66
|
end
|
|
67
|
+
@value = nil
|
|
58
68
|
adapter.set_attribute_value(@native, new_value)
|
|
59
69
|
end
|
|
60
70
|
|
data/lib/moxml/document.rb
CHANGED
|
@@ -29,6 +29,21 @@ module Moxml
|
|
|
29
29
|
(owner.is_a?(Document) && owner.root.equal?(element))
|
|
30
30
|
if owner.is_a?(Document) && attached &&
|
|
31
31
|
!owner.equal?(self) && element.context.equal?(context)
|
|
32
|
+
installed = adapter.adopt_root(@native, element.native)
|
|
33
|
+
if installed
|
|
34
|
+
# The engine deep-copies the foreign subtree into this
|
|
35
|
+
# document's pool (leptris >= 1.9.292, engine #1495) and the
|
|
36
|
+
# binding memoizes the INSTALLED handle — a fresh pointer.
|
|
37
|
+
# Re-point the source wrapper so later writes reach the live
|
|
38
|
+
# tree, and recompute its scope-derived memos.
|
|
39
|
+
unless installed.equal?(element.native)
|
|
40
|
+
element.refresh_native!(Moxml::Node.wrap(installed, context).native)
|
|
41
|
+
element.invalidate_namespace_cache!
|
|
42
|
+
end
|
|
43
|
+
context.bump_children_generation
|
|
44
|
+
invalidate_children_cache!
|
|
45
|
+
return
|
|
46
|
+
end
|
|
32
47
|
# libleptris refuses a root owned by another document (the
|
|
33
48
|
# engine's set_root ownership check), while Nokogiri adopts.
|
|
34
49
|
# Cross-document CHILD attaches are supported, so build the
|
data/lib/moxml/element.rb
CHANGED
|
@@ -189,6 +189,7 @@ module Moxml
|
|
|
189
189
|
raise ArgumentError, "children= accepts String, Node, or Array"
|
|
190
190
|
end
|
|
191
191
|
adapter.replace_children(@native, natives)
|
|
192
|
+
@text = nil
|
|
192
193
|
context.bump_children_generation
|
|
193
194
|
invalidate_children_cache!
|
|
194
195
|
end
|
|
@@ -329,25 +330,48 @@ module Moxml
|
|
|
329
330
|
end
|
|
330
331
|
|
|
331
332
|
def text
|
|
332
|
-
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
|
|
333
|
+
# Generation-gated memo: engines allocate a fresh String per read
|
|
334
|
+
# (Nokogiri node.text), and hydration walks re-read the same
|
|
335
|
+
# wrappers — Element#text was a top string allocation site in
|
|
336
|
+
# large-document metanorma compiles. Text only changes through
|
|
337
|
+
# this wrapper's writers (text=/children=), all of which clear
|
|
338
|
+
# the memo.
|
|
339
|
+
@text = nil if @text_gen != context.children_generation
|
|
340
|
+
@text_gen = context.children_generation
|
|
341
|
+
@text ||= begin
|
|
342
|
+
val = adapter.text_content(@native)
|
|
343
|
+
# Entity-free documents (the common case) skip the marker
|
|
344
|
+
# restore scans entirely — the per-wrapper entity_bearing?
|
|
345
|
+
# memo rides the adapter's serialize generation.
|
|
346
|
+
entity_bearing? ? adapter.restore_entities(val) : val
|
|
347
|
+
end
|
|
337
348
|
end
|
|
338
349
|
|
|
339
350
|
alias content text
|
|
340
351
|
|
|
341
352
|
def text=(content)
|
|
353
|
+
@text = nil
|
|
354
|
+
# A text write replaces the whole child list; the generation
|
|
355
|
+
# bump also retires @elements/@children memos across wrappers.
|
|
356
|
+
context.bump_children_generation
|
|
342
357
|
adapter.set_text_content(@native, normalize_xml_value(content))
|
|
343
358
|
invalidate_children_cache!
|
|
344
359
|
end
|
|
345
360
|
|
|
346
361
|
alias content= text=
|
|
347
362
|
|
|
363
|
+
# Generation-gated memo (the #321 shape): the native read
|
|
364
|
+
# allocates a fresh String per call and hydration walks re-read
|
|
365
|
+
# the same wrappers — 122k births over 10 passes of 2,000
|
|
366
|
+
# elements before this memo, 5 after. All inner-text writers are
|
|
367
|
+
# children mutations, which bump the children generation.
|
|
348
368
|
def inner_text
|
|
349
|
-
|
|
350
|
-
|
|
369
|
+
@inner_text = nil if @inner_text_gen != context.children_generation
|
|
370
|
+
@inner_text_gen = context.children_generation
|
|
371
|
+
@inner_text ||= begin
|
|
372
|
+
text = raw_inner_text
|
|
373
|
+
entity_bearing? ? adapter.restore_entities(text) : text
|
|
374
|
+
end
|
|
351
375
|
end
|
|
352
376
|
|
|
353
377
|
# Returns inner text without entity marker restoration.
|
|
@@ -363,6 +387,11 @@ module Moxml
|
|
|
363
387
|
def inner_xml=(xml)
|
|
364
388
|
wrapper = "_moxml_inner_#{Process.pid}_#{object_id}"
|
|
365
389
|
doc = context.parse("<#{wrapper}>#{xml}</#{wrapper}>")
|
|
390
|
+
@text = nil
|
|
391
|
+
# Whole-list replacement rides the children generation like
|
|
392
|
+
# children= does (moxml#310) — text and elements memos across
|
|
393
|
+
# wrappers recompute on next read.
|
|
394
|
+
context.bump_children_generation
|
|
366
395
|
adapter.replace_children(@native, doc.root.children.map(&:native))
|
|
367
396
|
invalidate_children_cache!
|
|
368
397
|
end
|
|
@@ -415,13 +444,23 @@ module Moxml
|
|
|
415
444
|
# too when the attribute set changes. No context generation
|
|
416
445
|
# bump — that would evict every wrapper's caches document-wide
|
|
417
446
|
# on each write of a bulk build.
|
|
447
|
+
# Local counter for held Attribute wrappers: value writes bump it
|
|
448
|
+
# (no context-wide bump — bulk builds would evict every wrapper's
|
|
449
|
+
# caches per write), and Attribute#value recomputes when its
|
|
450
|
+
# owner's counter moves.
|
|
451
|
+
def attribute_value_generation
|
|
452
|
+
@attribute_value_generation ||= 0
|
|
453
|
+
end
|
|
454
|
+
|
|
418
455
|
def invalidate_attribute_value_cache!
|
|
419
456
|
@attribute_cache = nil
|
|
457
|
+
@attribute_value_generation = attribute_value_generation + 1
|
|
420
458
|
end
|
|
421
459
|
|
|
422
460
|
def invalidate_local_attribute_cache!
|
|
423
461
|
@attributes = nil
|
|
424
462
|
@attribute_cache = nil
|
|
463
|
+
@attribute_value_generation = attribute_value_generation + 1
|
|
425
464
|
end
|
|
426
465
|
|
|
427
466
|
# Called by the namespace-scoped attribute paths (xmlns writes
|
data/lib/moxml/text.rb
CHANGED
|
@@ -5,8 +5,14 @@ module Moxml
|
|
|
5
5
|
include Node
|
|
6
6
|
|
|
7
7
|
def content
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Engines allocate a fresh String per read (Nokogiri
|
|
9
|
+
# node.text); hydration walks re-read the same text wrappers —
|
|
10
|
+
# this read was the single largest string allocation site in
|
|
11
|
+
# large-document metanorma compiles. Cleared by content=.
|
|
12
|
+
@content ||= begin
|
|
13
|
+
text = raw_content
|
|
14
|
+
entity_bearing? ? adapter.restore_entities(text) : text
|
|
15
|
+
end
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
# Returns raw content without entity marker restoration.
|
|
@@ -15,6 +21,7 @@ module Moxml
|
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
def content=(text)
|
|
24
|
+
@content = nil
|
|
18
25
|
adapter.set_text_content(@native, normalize_xml_value(text))
|
|
19
26
|
end
|
|
20
27
|
|
data/lib/moxml/version.rb
CHANGED
|
@@ -46,5 +46,19 @@ RSpec.describe Moxml::Adapter::Base do
|
|
|
46
46
|
expect(Moxml::Adapter::Leptris.children_accepts_entity_flag?).to be(true)
|
|
47
47
|
expect(Moxml::Adapter::Nokogiri.children_accepts_entity_flag?).to be(true)
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
it "assumes the keyword when Method#parameters returns no data" do
|
|
51
|
+
# Opal without arity checking answers a bare JS undefined for
|
|
52
|
+
# parameters — not even nil?. Exercise the decision through the
|
|
53
|
+
# real values the probe can see.
|
|
54
|
+
probe = described_class.children_params_flag(nil)
|
|
55
|
+
expect(probe).to be(true)
|
|
56
|
+
expect(described_class.children_params_flag([%i[req node]])).to be(false)
|
|
57
|
+
expect(
|
|
58
|
+
described_class.children_params_flag(
|
|
59
|
+
[%i[req node], %i[key entity_bearing]],
|
|
60
|
+
),
|
|
61
|
+
).to be(true)
|
|
62
|
+
end
|
|
49
63
|
end
|
|
50
64
|
end
|
|
@@ -312,6 +312,22 @@ RSpec.describe "Nokogiri-compatible node sugar" do
|
|
|
312
312
|
end
|
|
313
313
|
end
|
|
314
314
|
|
|
315
|
+
describe "Document#root= native adoption re-points the source wrapper" do
|
|
316
|
+
it "writes through the original wrapper reach the installed tree" do
|
|
317
|
+
ctx = Moxml.new(:leptris)
|
|
318
|
+
other = ctx.parse('<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>')
|
|
319
|
+
doc = ctx.create_document
|
|
320
|
+
src_root = other.root
|
|
321
|
+
doc.root = src_root
|
|
322
|
+
# the engine deep-copies; the wrapper now addresses the installed
|
|
323
|
+
# handle — a later write must serialize into the new document
|
|
324
|
+
src_root["a"] = "2"
|
|
325
|
+
expect(doc.to_xml(indent: 0)).to include('a="2"')
|
|
326
|
+
expect(doc.root["a"]).to eq("2")
|
|
327
|
+
expect(doc.root.namespace_uri.to_s).to eq("urn:x")
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
315
331
|
describe "Document#root= with a foreign root" do
|
|
316
332
|
it "adopts the subtree from another document" do
|
|
317
333
|
doc = Moxml.parse("<old><x/></old>")
|
|
@@ -396,4 +412,50 @@ RSpec.describe "Nokogiri-compatible node sugar" do
|
|
|
396
412
|
expect(doc.at("//term/definition/p").text).to eq("a")
|
|
397
413
|
end
|
|
398
414
|
end
|
|
415
|
+
|
|
416
|
+
describe "wrapper read memos" do
|
|
417
|
+
it "Element#text memoizes and invalidates on text=/children=" do
|
|
418
|
+
doc = Moxml.parse("<r><a>x<b>y</b>z</a></r>")
|
|
419
|
+
a = doc.at("//a")
|
|
420
|
+
first = a.text
|
|
421
|
+
expect(a.text).to equal(first)
|
|
422
|
+
a.text = "new"
|
|
423
|
+
expect(a.text).to eq("new")
|
|
424
|
+
a.children = "<c>w</c>"
|
|
425
|
+
expect(a.text).to eq("w")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
it "Element#inner_text memoizes and invalidates on child mutations" do
|
|
429
|
+
doc = Moxml.parse("<r><a>x<b>y</b>z</a></r>")
|
|
430
|
+
a = doc.at("//a")
|
|
431
|
+
first = a.inner_text
|
|
432
|
+
expect(a.inner_text).to equal(first)
|
|
433
|
+
a.add_child(doc.create_element("c"))
|
|
434
|
+
expect(a.inner_text).not_to equal(first)
|
|
435
|
+
a.text = "plain"
|
|
436
|
+
expect(a.inner_text).to eq("plain")
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it "Attribute#value memoizes and invalidates on value=" do
|
|
440
|
+
doc = Moxml.parse(%(<r a="1"/>))
|
|
441
|
+
attr = doc.root.attributes.first
|
|
442
|
+
first = attr.value
|
|
443
|
+
expect(attr.value).to equal(first)
|
|
444
|
+
attr.value = "2"
|
|
445
|
+
expect(attr.value).to eq("2")
|
|
446
|
+
expect(doc.root["a"]).to eq("2")
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
describe "Text#content memo" do
|
|
451
|
+
it "memoizes and invalidates on content=" do
|
|
452
|
+
doc = Moxml.parse("<r>hello</r>")
|
|
453
|
+
t = doc.root.children.first
|
|
454
|
+
first = t.content
|
|
455
|
+
expect(t.content).to equal(first)
|
|
456
|
+
expect(t.text).to equal(first)
|
|
457
|
+
t.content = "bye"
|
|
458
|
+
expect(t.content).to eq("bye")
|
|
459
|
+
end
|
|
460
|
+
end
|
|
399
461
|
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.106
|
|
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-04 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
|