leptris 1.9.12-aarch64-linux → 1.9.14-aarch64-linux
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/CHANGELOG.md +42 -0
- data/docs/adr/0003-readonly-memoization-pattern.md +17 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +6 -3
- data/lib/leptris/xml/comment.rb +6 -3
- data/lib/leptris/xml/document.rb +31 -7
- data/lib/leptris/xml/element.rb +42 -18
- data/lib/leptris/xml/node.rb +47 -15
- data/lib/leptris/xml/node_set.rb +0 -1
- data/lib/leptris/xml/processing_instruction.rb +12 -6
- data/lib/leptris/xml/text.rb +6 -3
- 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: c96f59841d0606a790ec372576cdb05f50bbc39c43cdf1188e7a4535068a7a8f
|
|
4
|
+
data.tar.gz: e3396a5ec39b5638f20a2254da0796f6330ca689f62e2fcad939850a42662f64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6cc9bb4c0ba7b0c39ced816d8435370947d6e051df16d1d03a899c5fbbdd1a7ba1e0c7e0a05f7879874a4b20c5db7b2f7b639138eb0137bbb1276e1288b7b59
|
|
7
|
+
data.tar.gz: 12846d96eec9f9a3d681bdb1bd9a2e29807a21575adc209851bdb4b3067f8c5999732faa9a4bdfad0af1ac6e55cdac6f52c832d886623f137c8b0aa867bc5ba2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.14] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Writable documents memoize** — the versioned read cache. A
|
|
13
|
+
per-document mutation version advances at every mutation gate
|
|
14
|
+
(ensure_writable!, root=, add_pi); each memoized field carries
|
|
15
|
+
its own version stamp and recomputes after any bump. Readonly
|
|
16
|
+
semantics unchanged (their version never advances — memos
|
|
17
|
+
forever valid). Measured on writable documents: namespace
|
|
18
|
+
inspection 0.392 s -> 0.013 s (**31x**, readonly parity),
|
|
19
|
+
children 0.154 s -> 0.014 s (**11x**). The read-heavy
|
|
20
|
+
DOM-editing workload (parse, query repeatedly, mutate
|
|
21
|
+
occasionally) now performs like the readonly one between
|
|
22
|
+
mutations. Staleness is test-pinned: a mutation-invalidation
|
|
23
|
+
matrix covers content, attribute, structural, namespace, root=,
|
|
24
|
+
and PI mutations against every memoized read. The shared
|
|
25
|
+
node-level stamp variant was tried and rejected in favor of
|
|
26
|
+
per-field stamps (one field's recompute must not resurrect
|
|
27
|
+
another's stale memo). ADR 0003 extended.
|
|
28
|
+
- NodeSet#[] drops the Ruby-side bounds FFI (the C accessor
|
|
29
|
+
already returns NULL out of range).
|
|
30
|
+
|
|
31
|
+
## [1.9.13] - 2026-08-27
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Cached-true readonly guard**: readonly is one-way, so a node
|
|
36
|
+
caches TRUE once observed (FALSE stays uncached — the document
|
|
37
|
+
may still flip). Per-read guards drop from a three-call document
|
|
38
|
+
round-trip to one ivar check. Harness attr loop 0.061 -> 0.048 s.
|
|
39
|
+
- **Lazy wrapper-cache allocation**: Documents no longer allocate
|
|
40
|
+
the wrapper-identity Hash up front; parse-heavy loops that free
|
|
41
|
+
before re-reading stop paying it (~9% on the tiny-doc
|
|
42
|
+
parse-query-serialize loop). Identity semantics unchanged.
|
|
43
|
+
|
|
44
|
+
### Measured dead
|
|
45
|
+
|
|
46
|
+
- Further readonly-[] trimming: the isolated read is at the Ruby
|
|
47
|
+
call-chain floor (~227 ns across ~7 calls); inlining the
|
|
48
|
+
attributes build would duplicate memo logic for ~10%.
|
|
49
|
+
|
|
8
50
|
## [1.9.12] - 2026-08-27
|
|
9
51
|
|
|
10
52
|
### Changed
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
# ADR 0003:
|
|
1
|
+
# ADR 0003: memoization stays hand-rolled; writable docs memoize via version stamps
|
|
2
|
+
|
|
3
|
+
## 2026-08-27 extension (leptris-ruby 1.9.14)
|
|
4
|
+
|
|
5
|
+
Writable documents now memoize too: Document carries a mutation
|
|
6
|
+
version advanced at every mutation gate (Node#ensure_writable!,
|
|
7
|
+
root=, add_pi), and each memoized field stores its OWN version
|
|
8
|
+
stamp (`return @x if memo_hit?(@x_version)`). A shared node-level
|
|
9
|
+
stamp was tried and rejected — one field's recompute resurrects
|
|
10
|
+
another field's stale memo. Readonly semantics are unchanged
|
|
11
|
+
(their version never advances). Round X's "version-stamp rejected:
|
|
12
|
+
the compare costs the read" was corrected: the compare is cheap
|
|
13
|
+
against multi-FFI reads.
|
|
14
|
+
|
|
15
|
+
The hand-rolled per-site pattern (below) still stands; only the
|
|
16
|
+
guard gained a version argument.
|
|
17
|
+
|
|
2
18
|
|
|
3
19
|
## Context
|
|
4
20
|
Every readonly-mode read repeats the same three lines:
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/cdata.rb
CHANGED
|
@@ -4,11 +4,14 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
4
4
|
def name; "#cdata-section"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
return @content if
|
|
7
|
+
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr)
|
|
10
|
-
|
|
9
|
+
result = Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr)
|
|
10
|
+
if @document
|
|
11
|
+
@content = result
|
|
12
|
+
@content_version = @document.version
|
|
11
13
|
end
|
|
14
|
+
result
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def content=(new_content)
|
data/lib/leptris/xml/comment.rb
CHANGED
|
@@ -4,11 +4,14 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
4
4
|
def name; "comment"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
return @content if
|
|
7
|
+
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr)
|
|
10
|
-
|
|
9
|
+
result = Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr)
|
|
10
|
+
if @document
|
|
11
|
+
@content = result
|
|
12
|
+
@content_version = @document.version
|
|
11
13
|
end
|
|
14
|
+
result
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def content=(new_content)
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "ffi"
|
|
4
4
|
|
|
5
5
|
class Leptris::XML::Document
|
|
6
|
-
attr_reader :c_ptr
|
|
6
|
+
attr_reader :c_ptr
|
|
7
7
|
|
|
8
8
|
# @api private
|
|
9
9
|
# Internal flag container shared between the Document instance and its
|
|
@@ -14,10 +14,27 @@ class Leptris::XML::Document
|
|
|
14
14
|
# `Document#free` was called explicitly and then GC ran.
|
|
15
15
|
Freed = Struct.new(:state) # state: :alive | :freed
|
|
16
16
|
|
|
17
|
+
# Mutation version: advanced by every data mutation (via
|
|
18
|
+
# Node#ensure_writable!, root=, add_pi). Node memos stamp the
|
|
19
|
+
# version they were computed under and recompute after any bump —
|
|
20
|
+
# the invalidation that makes WRITABLE-document memoization sound.
|
|
21
|
+
# Readonly documents never advance it, so their memos are forever
|
|
22
|
+
# valid (ADR 0003 semantics, unchanged).
|
|
23
|
+
def version
|
|
24
|
+
@version
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Called by the mutation gates (Node#ensure_writable!, root=,
|
|
28
|
+
# add_pi) — every memo stamped with an older version discards.
|
|
29
|
+
def advance_version
|
|
30
|
+
@version += 1
|
|
31
|
+
end
|
|
32
|
+
|
|
17
33
|
def initialize(c_ptr = nil, freed = Freed.new(:alive))
|
|
18
34
|
@c_ptr = c_ptr
|
|
19
35
|
@freed = freed
|
|
20
36
|
@readonly = false
|
|
37
|
+
@version = 0
|
|
21
38
|
# Per-document STRONG cache for Node wrappers, keyed on c_ptr
|
|
22
39
|
# address. Every wrapper is created through Node.wrap, which is the
|
|
23
40
|
# single construction path, so the same C node always yields the
|
|
@@ -31,7 +48,13 @@ class Leptris::XML::Document
|
|
|
31
48
|
# the weak map — any GC sweep evicted it and the second call built
|
|
32
49
|
# a fresh object. A strong cache costs at most one wrapper per node
|
|
33
50
|
# actually visited, held until the document dies.
|
|
34
|
-
|
|
51
|
+
#
|
|
52
|
+
# Allocated lazily: parse-heavy loops stop paying one Hash per
|
|
53
|
+
# document for trees that are freed before any wrap.
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wrapper_cache
|
|
57
|
+
@wrapper_cache ||= {}
|
|
35
58
|
end
|
|
36
59
|
|
|
37
60
|
def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
|
|
@@ -136,6 +159,7 @@ class Leptris::XML::Document
|
|
|
136
159
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
137
160
|
Leptris::XML::FFI.check_status(
|
|
138
161
|
Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
|
|
162
|
+
@version += 1
|
|
139
163
|
element
|
|
140
164
|
end
|
|
141
165
|
|
|
@@ -230,7 +254,7 @@ class Leptris::XML::Document
|
|
|
230
254
|
@freed.state = :freed
|
|
231
255
|
Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
|
|
232
256
|
@c_ptr = nil
|
|
233
|
-
@wrapper_cache
|
|
257
|
+
@wrapper_cache&.clear
|
|
234
258
|
end
|
|
235
259
|
|
|
236
260
|
# Enable the first-party EXSLT-style extension pack on this
|
|
@@ -247,15 +271,14 @@ class Leptris::XML::Document
|
|
|
247
271
|
# Document-level processing instructions (not tree nodes):
|
|
248
272
|
# an array of [target, data] pairs in document order.
|
|
249
273
|
def processing_instructions
|
|
250
|
-
if
|
|
251
|
-
return @processing_instructions
|
|
252
|
-
end
|
|
274
|
+
return @processing_instructions if @pi_version == @version
|
|
253
275
|
count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
|
|
254
276
|
result = count.times.map do |i|
|
|
255
277
|
[Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
|
|
256
278
|
Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
|
|
257
279
|
end
|
|
258
|
-
@processing_instructions = result
|
|
280
|
+
@processing_instructions = result
|
|
281
|
+
@pi_version = @version
|
|
259
282
|
result
|
|
260
283
|
end
|
|
261
284
|
|
|
@@ -264,6 +287,7 @@ class Leptris::XML::Document
|
|
|
264
287
|
witness = Leptris::XML::FFI.leptris_document_add_pi(
|
|
265
288
|
@c_ptr, target.to_s, data.to_s)
|
|
266
289
|
raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
|
|
290
|
+
@version += 1
|
|
267
291
|
self
|
|
268
292
|
end
|
|
269
293
|
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -17,11 +17,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
17
17
|
alias_method :node_name=, :name=
|
|
18
18
|
|
|
19
19
|
def content
|
|
20
|
-
return @content if
|
|
20
|
+
return @content if memo_hit?(@content_version)
|
|
21
21
|
ensure_alive!
|
|
22
|
-
Leptris::XML::FFI.leptris_element_text(@c_ptr)
|
|
23
|
-
|
|
22
|
+
result = Leptris::XML::FFI.leptris_element_text(@c_ptr)
|
|
23
|
+
if @document
|
|
24
|
+
@content = result
|
|
25
|
+
@content_version = @document.version
|
|
24
26
|
end
|
|
27
|
+
result
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def content=(new_content)
|
|
@@ -36,7 +39,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
36
39
|
# materializes on demand — repeated reads become hash lookups,
|
|
37
40
|
# with no dispatch and no lifetime guard (a hash read cannot
|
|
38
41
|
# use-after-free).
|
|
39
|
-
if
|
|
42
|
+
if readonly_document?
|
|
40
43
|
cached = attributes[key.to_s]
|
|
41
44
|
return cached.nil? ? nil : cached.value
|
|
42
45
|
end
|
|
@@ -104,31 +107,43 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
104
107
|
end
|
|
105
108
|
|
|
106
109
|
def keys
|
|
107
|
-
return @keys if
|
|
110
|
+
return @keys if memo_hit?(@keys_version)
|
|
108
111
|
result = each_attribute.to_a.map(&:name)
|
|
109
|
-
|
|
112
|
+
if @document
|
|
113
|
+
@keys = result
|
|
114
|
+
@keys_version = @document.version
|
|
115
|
+
end
|
|
110
116
|
result
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
def values
|
|
114
|
-
return @values if
|
|
120
|
+
return @values if memo_hit?(@values_version)
|
|
115
121
|
result = each_attribute.to_a.map(&:value)
|
|
116
|
-
|
|
122
|
+
if @document
|
|
123
|
+
@values = result
|
|
124
|
+
@values_version = @document.version
|
|
125
|
+
end
|
|
117
126
|
result
|
|
118
127
|
end
|
|
119
128
|
|
|
120
129
|
def attributes
|
|
121
|
-
return @attributes if
|
|
130
|
+
return @attributes if memo_hit?(@attributes_version)
|
|
122
131
|
result = {}
|
|
123
132
|
each_attribute { |attr| result[attr.name] = attr }
|
|
124
|
-
|
|
133
|
+
if @document
|
|
134
|
+
@attributes = result
|
|
135
|
+
@attributes_version = @document.version
|
|
136
|
+
end
|
|
125
137
|
result
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
def attribute_nodes
|
|
129
|
-
return @attribute_nodes if
|
|
141
|
+
return @attribute_nodes if memo_hit?(@attribute_nodes_version)
|
|
130
142
|
result = each_attribute.to_a
|
|
131
|
-
|
|
143
|
+
if @document
|
|
144
|
+
@attribute_nodes = result
|
|
145
|
+
@attribute_nodes_version = @document.version
|
|
146
|
+
end
|
|
132
147
|
result
|
|
133
148
|
end
|
|
134
149
|
|
|
@@ -247,7 +262,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
247
262
|
alias_method :<<, :add_child
|
|
248
263
|
|
|
249
264
|
def namespace
|
|
250
|
-
return @namespace if
|
|
265
|
+
return @namespace if memo_hit?(@namespace_version)
|
|
251
266
|
ensure_alive!
|
|
252
267
|
uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
253
268
|
result =
|
|
@@ -261,12 +276,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
261
276
|
prefix = nil if prefix.nil? || prefix.empty?
|
|
262
277
|
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
263
278
|
end
|
|
264
|
-
|
|
279
|
+
if @document
|
|
280
|
+
@namespace = result
|
|
281
|
+
@namespace_version = @document.version
|
|
282
|
+
end
|
|
265
283
|
result
|
|
266
284
|
end
|
|
267
285
|
|
|
268
286
|
def namespace_definitions
|
|
269
|
-
return @namespace_definitions if
|
|
287
|
+
return @namespace_definitions if memo_hit?(@namespace_definitions_version)
|
|
270
288
|
ensure_alive!
|
|
271
289
|
count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
|
|
272
290
|
result = count.times.map do |i|
|
|
@@ -274,12 +292,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
274
292
|
uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
|
|
275
293
|
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
276
294
|
end
|
|
277
|
-
|
|
295
|
+
if @document
|
|
296
|
+
@namespace_definitions = result
|
|
297
|
+
@namespace_definitions_version = @document.version
|
|
298
|
+
end
|
|
278
299
|
result
|
|
279
300
|
end
|
|
280
301
|
|
|
281
302
|
def namespaces
|
|
282
|
-
return @namespaces if
|
|
303
|
+
return @namespaces if memo_hit?(@namespaces_version)
|
|
283
304
|
scopes = {}
|
|
284
305
|
node = self
|
|
285
306
|
while node.is_a?(Leptris::XML::Element)
|
|
@@ -289,7 +310,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
289
310
|
end
|
|
290
311
|
node = node.parent
|
|
291
312
|
end
|
|
292
|
-
|
|
313
|
+
if @document
|
|
314
|
+
@namespaces = scopes
|
|
315
|
+
@namespaces_version = @document.version
|
|
316
|
+
end
|
|
293
317
|
scopes
|
|
294
318
|
end
|
|
295
319
|
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -101,13 +101,29 @@ class Leptris::XML::Node
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Raises ReadOnlyError when the owning document was marked readonly,
|
|
104
|
-
# UseAfterFreeError when it was freed.
|
|
104
|
+
# UseAfterFreeError when it was freed. Every node-level mutation
|
|
105
|
+
# passes through this gate, so it is where the document's mutation
|
|
106
|
+
# version advances — the invalidation behind writable-document
|
|
107
|
+
# memoization. Bumping before the C call is conservative: a failed
|
|
108
|
+
# mutation merely discards memos.
|
|
105
109
|
def ensure_writable!
|
|
106
110
|
ensure_alive!
|
|
107
|
-
if
|
|
111
|
+
if readonly_document?
|
|
108
112
|
raise Leptris::XML::ReadOnlyError,
|
|
109
113
|
"document is readonly — mutation attempted on #{inspect}"
|
|
110
114
|
end
|
|
115
|
+
@document.advance_version
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Readonly is one-way, so caching TRUE is sound: once observed,
|
|
120
|
+
# the document is readonly forever. FALSE stays uncached (the
|
|
121
|
+
# document may still flip). Saves the document round-trip on
|
|
122
|
+
# per-read guards.
|
|
123
|
+
def readonly_document?
|
|
124
|
+
return true if instance_variable_defined?(:@readonly_document)
|
|
125
|
+
return false unless @document&.readonly?
|
|
126
|
+
@readonly_document = true
|
|
111
127
|
end
|
|
112
128
|
|
|
113
129
|
def line
|
|
@@ -132,14 +148,17 @@ class Leptris::XML::Node
|
|
|
132
148
|
def children
|
|
133
149
|
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
134
150
|
# construction is paid once.
|
|
135
|
-
return @children if
|
|
151
|
+
return @children if memo_hit?(@children_version)
|
|
136
152
|
ensure_alive!
|
|
137
153
|
parent = as_element_or_self
|
|
138
154
|
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
139
155
|
Leptris::XML::Node.wrap(ptr, @document, parent: parent)
|
|
140
156
|
end
|
|
141
157
|
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
142
|
-
|
|
158
|
+
if @document
|
|
159
|
+
@children = result
|
|
160
|
+
@children_version = @document.version
|
|
161
|
+
end
|
|
143
162
|
result
|
|
144
163
|
end
|
|
145
164
|
|
|
@@ -175,9 +194,12 @@ class Leptris::XML::Node
|
|
|
175
194
|
end
|
|
176
195
|
|
|
177
196
|
def element_children
|
|
178
|
-
return @element_children if
|
|
197
|
+
return @element_children if memo_hit?(@element_children_version)
|
|
179
198
|
result = children.select(&:element?)
|
|
180
|
-
|
|
199
|
+
if @document
|
|
200
|
+
@element_children = result
|
|
201
|
+
@element_children_version = @document.version
|
|
202
|
+
end
|
|
181
203
|
result
|
|
182
204
|
end
|
|
183
205
|
alias_method :elements, :element_children
|
|
@@ -220,16 +242,19 @@ class Leptris::XML::Node
|
|
|
220
242
|
end
|
|
221
243
|
|
|
222
244
|
def path
|
|
223
|
-
return @path if
|
|
245
|
+
return @path if memo_hit?(@path_version)
|
|
224
246
|
ensure_alive!
|
|
225
247
|
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
|
|
226
248
|
result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
227
|
-
|
|
249
|
+
if @document
|
|
250
|
+
@path = result
|
|
251
|
+
@path_version = @document.version
|
|
252
|
+
end
|
|
228
253
|
result
|
|
229
254
|
end
|
|
230
255
|
|
|
231
256
|
def css_path
|
|
232
|
-
return @css_path if
|
|
257
|
+
return @css_path if memo_hit?(@css_path_version)
|
|
233
258
|
result =
|
|
234
259
|
if path.nil?
|
|
235
260
|
nil
|
|
@@ -239,7 +264,10 @@ class Leptris::XML::Node
|
|
|
239
264
|
part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
|
|
240
265
|
end.join(" > ")
|
|
241
266
|
end
|
|
242
|
-
|
|
267
|
+
if @document
|
|
268
|
+
@css_path = result
|
|
269
|
+
@css_path_version = @document.version
|
|
270
|
+
end
|
|
243
271
|
result
|
|
244
272
|
end
|
|
245
273
|
|
|
@@ -264,11 +292,15 @@ class Leptris::XML::Node
|
|
|
264
292
|
|
|
265
293
|
protected
|
|
266
294
|
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
# A memo is valid while the document's mutation version has not
|
|
296
|
+
# advanced since that memo was stored. Each memoized field carries
|
|
297
|
+
# its OWN stamp — a shared node-level stamp would let one field's
|
|
298
|
+
# recompute resurrect another field's stale memo. Readonly
|
|
299
|
+
# documents never advance the version, so their memos are forever
|
|
300
|
+
# valid (ADR 0003 semantics); writable documents gain memos
|
|
301
|
+
# between mutations.
|
|
302
|
+
def memo_hit?(stamp)
|
|
303
|
+
@document && stamp == @document.version
|
|
272
304
|
end
|
|
273
305
|
|
|
274
306
|
def as_element_or_self
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -60,7 +60,6 @@ class Leptris::XML::NodeSet
|
|
|
60
60
|
# inconsistently with an eager set.
|
|
61
61
|
return to_a[idx] if idx.negative?
|
|
62
62
|
if @result_ptr
|
|
63
|
-
return nil if idx < 0 || idx >= length
|
|
64
63
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
|
|
65
64
|
return nil if ptr.null?
|
|
66
65
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
4
4
|
def name
|
|
5
|
-
return @name if
|
|
5
|
+
return @name if memo_hit?(@name_version)
|
|
6
6
|
ensure_alive!
|
|
7
|
-
Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
|
|
8
|
-
|
|
7
|
+
result = Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
|
|
8
|
+
if @document
|
|
9
|
+
@name = result
|
|
10
|
+
@name_version = @document.version
|
|
9
11
|
end
|
|
12
|
+
result
|
|
10
13
|
end
|
|
11
14
|
alias_method :target, :name
|
|
12
15
|
|
|
13
16
|
def content
|
|
14
|
-
return @content if
|
|
17
|
+
return @content if memo_hit?(@content_version)
|
|
15
18
|
ensure_alive!
|
|
16
|
-
Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s
|
|
17
|
-
|
|
19
|
+
result = Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s
|
|
20
|
+
if @document
|
|
21
|
+
@content = result
|
|
22
|
+
@content_version = @document.version
|
|
18
23
|
end
|
|
24
|
+
result
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def target=(new_target)
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -4,11 +4,14 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
4
4
|
def name; "text"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
return @content if
|
|
7
|
+
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
|
|
10
|
-
|
|
9
|
+
result = Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
|
|
10
|
+
if @document
|
|
11
|
+
@content = result
|
|
12
|
+
@content_version = @document.version
|
|
11
13
|
end
|
|
14
|
+
result
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def content=(new_content)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.14
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|