moxml 0.5.103 → 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/README.adoc +2 -1
- data/Rakefile +10 -6
- data/lib/moxml/adapter/base.rb +22 -4
- data/lib/moxml/adapter/leptris.rb +10 -1
- data/lib/moxml/attribute.rb +21 -11
- data/lib/moxml/element.rb +35 -5
- data/lib/moxml/node.rb +8 -0
- 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 +48 -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: 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/README.adoc
CHANGED
|
@@ -1283,7 +1283,8 @@ reproduce:
|
|
|
1283
1283
|
[source,shell]
|
|
1284
1284
|
----
|
|
1285
1285
|
# One-time setup: install ragel + ruby-ll so the forks' gitignored
|
|
1286
|
-
# generated lexer/parser sources can be produced.
|
|
1286
|
+
# generated lexer/parser sources can be produced. ruby-ll must be
|
|
1287
|
+
# installed outside the bundle: vendor:prepare runs it outside Bundler.
|
|
1287
1288
|
gem install ruby-ll --no-document
|
|
1288
1289
|
# (Install ragel via your package manager, e.g. apt-get install ragel
|
|
1289
1290
|
# or brew install ragel.)
|
data/Rakefile
CHANGED
|
@@ -88,13 +88,17 @@ namespace :vendor do
|
|
|
88
88
|
"#{ruby_ll}/lib/ll/parser.rll"],
|
|
89
89
|
]
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
# Under bundle exec, ruby-ll resolves to the vendored opal-ruby-ll
|
|
92
|
+
# binstub, which needs the not-yet-compiled libll.
|
|
93
|
+
Bundler.with_unbundled_env do
|
|
94
|
+
generators.each do |cmd, output, source|
|
|
95
|
+
if File.exist?(output) && File.mtime(output) >= File.mtime(source)
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
100
|
+
sh cmd
|
|
94
101
|
end
|
|
95
|
-
|
|
96
|
-
FileUtils.mkdir_p(File.dirname(output))
|
|
97
|
-
sh cmd
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
|
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
|
|
@@ -2008,7 +2008,16 @@ module Moxml
|
|
|
2008
2008
|
)
|
|
2009
2009
|
end
|
|
2010
2010
|
else
|
|
2011
|
-
|
|
2011
|
+
begin
|
|
2012
|
+
node.unlink
|
|
2013
|
+
rescue ::Leptris::XML::Error => e
|
|
2014
|
+
# "Not found" from unlink means the node is already in
|
|
2015
|
+
# no parent list — detached (replace, children=, engine
|
|
2016
|
+
# paths the wrapper layer cannot clear links for).
|
|
2017
|
+
# Nokogiri's unlink is a no-op there; match it instead
|
|
2018
|
+
# of crashing cleanup passes.
|
|
2019
|
+
raise unless e.message.include?("Not found")
|
|
2020
|
+
end
|
|
2012
2021
|
end
|
|
2013
2022
|
end
|
|
2014
2023
|
|
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/node.rb
CHANGED
|
@@ -254,6 +254,11 @@ module Moxml
|
|
|
254
254
|
context.bump_children_generation
|
|
255
255
|
invalidate_parent_children_cache!
|
|
256
256
|
adapter.remove(@native)
|
|
257
|
+
# The wrapper-side parent link dies with the attach — leaving it
|
|
258
|
+
# stale makes a later #remove's guard trust a parent that no
|
|
259
|
+
# longer exists and push an already-detached node into the
|
|
260
|
+
# engine (leptris: "Not found").
|
|
261
|
+
@parent_node = nil
|
|
257
262
|
invalidate_children_cache!
|
|
258
263
|
# The detached subtree left its declaring ancestors behind
|
|
259
264
|
invalidate_namespace_cache!
|
|
@@ -269,6 +274,9 @@ module Moxml
|
|
|
269
274
|
node = prepare_node(node)
|
|
270
275
|
invalidate_parent_children_cache!
|
|
271
276
|
adapter.replace(@native, node.native)
|
|
277
|
+
# Self is detached by the replace — drop the stale parent link
|
|
278
|
+
# (see #remove).
|
|
279
|
+
@parent_node = nil
|
|
272
280
|
invalidate_children_cache!
|
|
273
281
|
self
|
|
274
282
|
end
|
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
|
|
@@ -383,4 +383,52 @@ RSpec.describe "Nokogiri-compatible node sugar" do
|
|
|
383
383
|
expect(doc.root.inner_xml).to eq("<a>x</a><b/>")
|
|
384
384
|
end
|
|
385
385
|
end
|
|
386
|
+
|
|
387
|
+
describe "Node#remove after replace with a parent link" do
|
|
388
|
+
it "is a no-op, not an engine error (stale wrapper parent link)" do
|
|
389
|
+
doc = Moxml.parse("<r><term><p>a</p><note>n</note></term></r>")
|
|
390
|
+
# children walks are parent-aware: the wrapper carries @parent_node
|
|
391
|
+
p_node = doc.at("//term").children.find { |c| c.name == "p" }
|
|
392
|
+
t = doc.create_element("definition")
|
|
393
|
+
p_node.replace(t)
|
|
394
|
+
expect { t << p_node.remove }.not_to raise_error
|
|
395
|
+
expect(doc.at("//term/definition/p")).not_to be_nil
|
|
396
|
+
expect(doc.at("//term/definition/p").text).to eq("a")
|
|
397
|
+
end
|
|
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
|
|
386
434
|
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.105
|
|
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-03 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
|