moxml 0.5.104 → 0.5.105
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 +22 -4
- data/lib/moxml/attribute.rb +21 -11
- data/lib/moxml/element.rb +35 -5
- 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 +35 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c57aa62d4c524daa9096eb565711a4ebbed7525ba851cb6e327c98de842c4e4
|
|
4
|
+
data.tar.gz: 604a922868c9ba3fb656c41e84e7b2f848bdd20defcd5259d697da32e2fd7e0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80f4dc813942807549d58b93492e731dceeb3528e7cfcfd3f3a3d4c5fbd1a2c4d5b234cbc1cad9b8945efc1c280e4408b373ac8d0bf2e489051277b98716fc82
|
|
7
|
+
data.tar.gz: c91474a18a6877d6c0a012765f27c37c2ccd42260c810b130d62c3d69a6f55f2e08019a6472bb6f1b5d1f8649f99c3747f0139c72a80a606322850e56a98f134
|
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
|
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/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,16 +330,30 @@ 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
|
|
@@ -363,6 +378,11 @@ module Moxml
|
|
|
363
378
|
def inner_xml=(xml)
|
|
364
379
|
wrapper = "_moxml_inner_#{Process.pid}_#{object_id}"
|
|
365
380
|
doc = context.parse("<#{wrapper}>#{xml}</#{wrapper}>")
|
|
381
|
+
@text = nil
|
|
382
|
+
# Whole-list replacement rides the children generation like
|
|
383
|
+
# children= does (moxml#310) — text and elements memos across
|
|
384
|
+
# wrappers recompute on next read.
|
|
385
|
+
context.bump_children_generation
|
|
366
386
|
adapter.replace_children(@native, doc.root.children.map(&:native))
|
|
367
387
|
invalidate_children_cache!
|
|
368
388
|
end
|
|
@@ -415,13 +435,23 @@ module Moxml
|
|
|
415
435
|
# too when the attribute set changes. No context generation
|
|
416
436
|
# bump — that would evict every wrapper's caches document-wide
|
|
417
437
|
# on each write of a bulk build.
|
|
438
|
+
# Local counter for held Attribute wrappers: value writes bump it
|
|
439
|
+
# (no context-wide bump — bulk builds would evict every wrapper's
|
|
440
|
+
# caches per write), and Attribute#value recomputes when its
|
|
441
|
+
# owner's counter moves.
|
|
442
|
+
def attribute_value_generation
|
|
443
|
+
@attribute_value_generation ||= 0
|
|
444
|
+
end
|
|
445
|
+
|
|
418
446
|
def invalidate_attribute_value_cache!
|
|
419
447
|
@attribute_cache = nil
|
|
448
|
+
@attribute_value_generation = attribute_value_generation + 1
|
|
420
449
|
end
|
|
421
450
|
|
|
422
451
|
def invalidate_local_attribute_cache!
|
|
423
452
|
@attributes = nil
|
|
424
453
|
@attribute_cache = nil
|
|
454
|
+
@attribute_value_generation = attribute_value_generation + 1
|
|
425
455
|
end
|
|
426
456
|
|
|
427
457
|
# 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
|
|
@@ -396,4 +396,39 @@ RSpec.describe "Nokogiri-compatible node sugar" do
|
|
|
396
396
|
expect(doc.at("//term/definition/p").text).to eq("a")
|
|
397
397
|
end
|
|
398
398
|
end
|
|
399
|
+
|
|
400
|
+
describe "wrapper read memos" do
|
|
401
|
+
it "Element#text memoizes and invalidates on text=/children=" do
|
|
402
|
+
doc = Moxml.parse("<r><a>x<b>y</b>z</a></r>")
|
|
403
|
+
a = doc.at("//a")
|
|
404
|
+
first = a.text
|
|
405
|
+
expect(a.text).to equal(first)
|
|
406
|
+
a.text = "new"
|
|
407
|
+
expect(a.text).to eq("new")
|
|
408
|
+
a.children = "<c>w</c>"
|
|
409
|
+
expect(a.text).to eq("w")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it "Attribute#value memoizes and invalidates on value=" do
|
|
413
|
+
doc = Moxml.parse(%(<r a="1"/>))
|
|
414
|
+
attr = doc.root.attributes.first
|
|
415
|
+
first = attr.value
|
|
416
|
+
expect(attr.value).to equal(first)
|
|
417
|
+
attr.value = "2"
|
|
418
|
+
expect(attr.value).to eq("2")
|
|
419
|
+
expect(doc.root["a"]).to eq("2")
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
describe "Text#content memo" do
|
|
424
|
+
it "memoizes and invalidates on content=" do
|
|
425
|
+
doc = Moxml.parse("<r>hello</r>")
|
|
426
|
+
t = doc.root.children.first
|
|
427
|
+
first = t.content
|
|
428
|
+
expect(t.content).to equal(first)
|
|
429
|
+
expect(t.text).to equal(first)
|
|
430
|
+
t.content = "bye"
|
|
431
|
+
expect(t.content).to eq("bye")
|
|
432
|
+
end
|
|
433
|
+
end
|
|
399
434
|
end
|