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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 566f2842eac4e01753965cb5f56eada53d6683b3ef6b576015eb4b0da572ae67
4
- data.tar.gz: ccc15ef083358a018e1536983017c6681c70c11ccbb4678ac123e5b0b2c9aada
3
+ metadata.gz: 0c57aa62d4c524daa9096eb565711a4ebbed7525ba851cb6e327c98de842c4e4
4
+ data.tar.gz: 604a922868c9ba3fb656c41e84e7b2f848bdd20defcd5259d697da32e2fd7e0e
5
5
  SHA512:
6
- metadata.gz: 2c29296b60bedabbc512c9f5c454b0195b3632dbf9952b0c397fc3bad6554135dd3096a04a34e1abddf9ce40088af4856b2ead7d8ca69a4eb251dc3db38286b3
7
- data.tar.gz: fd1684a85d2ace2191b33bf996a12e8bea6cb535a340732cccee3318abc6649ba2e07548509d1469959e10ee711ecec650f8ec6b4525beb6a66e2c9a75a81c1d
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
- generators.each do |cmd, output, source|
92
- if File.exist?(output) && File.mtime(output) >= File.mtime(source)
93
- next
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
 
@@ -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
- kinds = %i[key keyrest keyreq]
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.any? do |kind, _name|
137
- kinds.include?(kind)
138
- end
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
- node.unlink
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
 
@@ -27,19 +27,28 @@ module Moxml
27
27
  end
28
28
 
29
29
  def value
30
- val = @native.value.to_s
31
- # Same guard as Element#text: entity-free documents skip the
32
- # marker restore scans. The memo rides the owning element's
33
- # (attr natives have no entity probe); a detached attribute
34
- # wrapper falls back to the unconditional restore.
35
- parent = @parent_node
36
- if parent.nil? || parent.entity_bearing?
37
- adapter.restore_entities(val)
38
- else
39
- val
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
- val = adapter.text_content(@native)
333
- # Entity-free documents (the common case) skip the marker
334
- # restore scans entirely — the per-wrapper entity_bearing?
335
- # memo rides the adapter's serialize generation.
336
- entity_bearing? ? adapter.restore_entities(val) : val
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
- text = raw_content
9
- entity_bearing? ? adapter.restore_entities(text) : text
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.103"
4
+ VERSION = "0.5.105"
5
5
  end
@@ -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.103
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-02 00:00:00.000000000 Z
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