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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bbf00c0bac03066dd77a5bb5f952ac786a893d51dd0b18365a67500bdad7a15
4
- data.tar.gz: 79d1609f628fcf8b69044ee5a9c990ae347cf0ff11c8b5e2ed2f812bee80ae0e
3
+ metadata.gz: 0c57aa62d4c524daa9096eb565711a4ebbed7525ba851cb6e327c98de842c4e4
4
+ data.tar.gz: 604a922868c9ba3fb656c41e84e7b2f848bdd20defcd5259d697da32e2fd7e0e
5
5
  SHA512:
6
- metadata.gz: 756a39e9fc200fc4b1a9880f820489684169c3206739d02bb44aad987bfa9b20140a65ae923b29745609fd47407927be9d369f4ed4a64d4129e0f3168f3aec56
7
- data.tar.gz: 6efc68d693d8c3b12f90ee4bfed2bf801162b89c7f3a5b0b1fc21eed45183a1ea962d4f2916a30932314208cd5099245a4977a0020b28892a83f4f019b40e03e
6
+ metadata.gz: 80f4dc813942807549d58b93492e731dceeb3528e7cfcfd3f3a3d4c5fbd1a2c4d5b234cbc1cad9b8945efc1c280e4408b373ac8d0bf2e489051277b98716fc82
7
+ data.tar.gz: c91474a18a6877d6c0a012765f27c37c2ccd42260c810b130d62c3d69a6f55f2e08019a6472bb6f1b5d1f8649f99c3747f0139c72a80a606322850e56a98f134
@@ -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
@@ -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/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.104"
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
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.104
4
+ version: 0.5.105
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.