leptris 1.9.13 → 1.9.15
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 +40 -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 +22 -4
- data/lib/leptris/xml/element.rb +52 -24
- data/lib/leptris/xml/node.rb +47 -16
- 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: c1b73d7abca12056f163aed086825140416384075955e593f11191156db475fc
|
|
4
|
+
data.tar.gz: ead9075339cfd0736ca79cbede18612702f4632c95d9d90a512f67b3e4c32ec7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22ec81a95a094913096d9c0749c42a64274f7af9225c4128b51529c8160f6480d3f23476b548f7c766202be9744dee9a830eaa715dbeea77a42e4d374f61b6e5
|
|
7
|
+
data.tar.gz: 5e5408837f3ca2b4b54e4069a36798000b67d87fee9ed7021927b4e6c8b95d6a04a9f9d770fcd786763569d700182f87fc5b7df54c14462391c0420f69d108ed
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.15] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`Element#[]` serves bare names from the versioned attributes
|
|
13
|
+
hash on every document kind** (writable included): materializes
|
|
14
|
+
on demand, invalidates through the mutation gate. Qualified names
|
|
15
|
+
(with a colon) still route to the engine — they resolve through
|
|
16
|
+
in-scope declarations where the written prefix never matters,
|
|
17
|
+
which a written-name-keyed hash cannot answer. Writable
|
|
18
|
+
attribute loops reach readonly parity: 0.196 s -> 0.101 s (2x);
|
|
19
|
+
the readonly special-case branch is deleted. The qualified/bare
|
|
20
|
+
split is spec-pinned (cross-prefix match, undeclared-prefix nil).
|
|
21
|
+
- **`first_element_child` memoizes** with version invalidation
|
|
22
|
+
(structural mutations bump): 0.058 s -> 0.015 s (4x) on the
|
|
23
|
+
repeat loop.
|
|
24
|
+
|
|
25
|
+
## [1.9.14] - 2026-08-27
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Writable documents memoize** — the versioned read cache. A
|
|
30
|
+
per-document mutation version advances at every mutation gate
|
|
31
|
+
(ensure_writable!, root=, add_pi); each memoized field carries
|
|
32
|
+
its own version stamp and recomputes after any bump. Readonly
|
|
33
|
+
semantics unchanged (their version never advances — memos
|
|
34
|
+
forever valid). Measured on writable documents: namespace
|
|
35
|
+
inspection 0.392 s -> 0.013 s (**31x**, readonly parity),
|
|
36
|
+
children 0.154 s -> 0.014 s (**11x**). The read-heavy
|
|
37
|
+
DOM-editing workload (parse, query repeatedly, mutate
|
|
38
|
+
occasionally) now performs like the readonly one between
|
|
39
|
+
mutations. Staleness is test-pinned: a mutation-invalidation
|
|
40
|
+
matrix covers content, attribute, structural, namespace, root=,
|
|
41
|
+
and PI mutations against every memoized read. The shared
|
|
42
|
+
node-level stamp variant was tried and rejected in favor of
|
|
43
|
+
per-field stamps (one field's recompute must not resurrect
|
|
44
|
+
another's stale memo). ADR 0003 extended.
|
|
45
|
+
- NodeSet#[] drops the Ruby-side bounds FFI (the C accessor
|
|
46
|
+
already returns NULL out of range).
|
|
47
|
+
|
|
8
48
|
## [1.9.13] - 2026-08-27
|
|
9
49
|
|
|
10
50
|
### 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
|
@@ -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
|
|
@@ -142,6 +159,7 @@ class Leptris::XML::Document
|
|
|
142
159
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
143
160
|
Leptris::XML::FFI.check_status(
|
|
144
161
|
Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
|
|
162
|
+
@version += 1
|
|
145
163
|
element
|
|
146
164
|
end
|
|
147
165
|
|
|
@@ -253,15 +271,14 @@ class Leptris::XML::Document
|
|
|
253
271
|
# Document-level processing instructions (not tree nodes):
|
|
254
272
|
# an array of [target, data] pairs in document order.
|
|
255
273
|
def processing_instructions
|
|
256
|
-
if
|
|
257
|
-
return @processing_instructions
|
|
258
|
-
end
|
|
274
|
+
return @processing_instructions if @pi_version == @version
|
|
259
275
|
count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
|
|
260
276
|
result = count.times.map do |i|
|
|
261
277
|
[Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
|
|
262
278
|
Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
|
|
263
279
|
end
|
|
264
|
-
@processing_instructions = result
|
|
280
|
+
@processing_instructions = result
|
|
281
|
+
@pi_version = @version
|
|
265
282
|
result
|
|
266
283
|
end
|
|
267
284
|
|
|
@@ -270,6 +287,7 @@ class Leptris::XML::Document
|
|
|
270
287
|
witness = Leptris::XML::FFI.leptris_document_add_pi(
|
|
271
288
|
@c_ptr, target.to_s, data.to_s)
|
|
272
289
|
raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
|
|
290
|
+
@version += 1
|
|
273
291
|
self
|
|
274
292
|
end
|
|
275
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)
|
|
@@ -32,16 +35,20 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def [](key)
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# BARE names serve from the versioned attributes hash (written
|
|
39
|
+
# name == the only legal key for a no-namespace attribute);
|
|
40
|
+
# materializes on demand, invalidates through the mutation gate
|
|
41
|
+
# (ADR 0003's 1.9.14 extension); a hash read cannot
|
|
42
|
+
# use-after-free. QUALIFIED names (with a colon) go to the
|
|
43
|
+
# engine: they resolve through in-scope declarations, where the
|
|
44
|
+
# written prefix never matters — the hash cannot answer them.
|
|
45
|
+
name = key.to_s
|
|
46
|
+
if @document && !name.include?(":")
|
|
47
|
+
cached = attributes[name]
|
|
41
48
|
return cached.nil? ? nil : cached.value
|
|
42
49
|
end
|
|
43
50
|
ensure_alive!
|
|
44
|
-
Leptris::XML::FFI.leptris_element_attribute(@c_ptr,
|
|
51
|
+
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
|
|
45
52
|
end
|
|
46
53
|
alias_method :attr, :[]
|
|
47
54
|
alias_method :get_attribute, :[]
|
|
@@ -104,31 +111,43 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
def keys
|
|
107
|
-
return @keys if
|
|
114
|
+
return @keys if memo_hit?(@keys_version)
|
|
108
115
|
result = each_attribute.to_a.map(&:name)
|
|
109
|
-
|
|
116
|
+
if @document
|
|
117
|
+
@keys = result
|
|
118
|
+
@keys_version = @document.version
|
|
119
|
+
end
|
|
110
120
|
result
|
|
111
121
|
end
|
|
112
122
|
|
|
113
123
|
def values
|
|
114
|
-
return @values if
|
|
124
|
+
return @values if memo_hit?(@values_version)
|
|
115
125
|
result = each_attribute.to_a.map(&:value)
|
|
116
|
-
|
|
126
|
+
if @document
|
|
127
|
+
@values = result
|
|
128
|
+
@values_version = @document.version
|
|
129
|
+
end
|
|
117
130
|
result
|
|
118
131
|
end
|
|
119
132
|
|
|
120
133
|
def attributes
|
|
121
|
-
return @attributes if
|
|
134
|
+
return @attributes if memo_hit?(@attributes_version)
|
|
122
135
|
result = {}
|
|
123
136
|
each_attribute { |attr| result[attr.name] = attr }
|
|
124
|
-
|
|
137
|
+
if @document
|
|
138
|
+
@attributes = result
|
|
139
|
+
@attributes_version = @document.version
|
|
140
|
+
end
|
|
125
141
|
result
|
|
126
142
|
end
|
|
127
143
|
|
|
128
144
|
def attribute_nodes
|
|
129
|
-
return @attribute_nodes if
|
|
145
|
+
return @attribute_nodes if memo_hit?(@attribute_nodes_version)
|
|
130
146
|
result = each_attribute.to_a
|
|
131
|
-
|
|
147
|
+
if @document
|
|
148
|
+
@attribute_nodes = result
|
|
149
|
+
@attribute_nodes_version = @document.version
|
|
150
|
+
end
|
|
132
151
|
result
|
|
133
152
|
end
|
|
134
153
|
|
|
@@ -247,7 +266,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
247
266
|
alias_method :<<, :add_child
|
|
248
267
|
|
|
249
268
|
def namespace
|
|
250
|
-
return @namespace if
|
|
269
|
+
return @namespace if memo_hit?(@namespace_version)
|
|
251
270
|
ensure_alive!
|
|
252
271
|
uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
253
272
|
result =
|
|
@@ -261,12 +280,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
261
280
|
prefix = nil if prefix.nil? || prefix.empty?
|
|
262
281
|
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
263
282
|
end
|
|
264
|
-
|
|
283
|
+
if @document
|
|
284
|
+
@namespace = result
|
|
285
|
+
@namespace_version = @document.version
|
|
286
|
+
end
|
|
265
287
|
result
|
|
266
288
|
end
|
|
267
289
|
|
|
268
290
|
def namespace_definitions
|
|
269
|
-
return @namespace_definitions if
|
|
291
|
+
return @namespace_definitions if memo_hit?(@namespace_definitions_version)
|
|
270
292
|
ensure_alive!
|
|
271
293
|
count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
|
|
272
294
|
result = count.times.map do |i|
|
|
@@ -274,12 +296,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
274
296
|
uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
|
|
275
297
|
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
276
298
|
end
|
|
277
|
-
|
|
299
|
+
if @document
|
|
300
|
+
@namespace_definitions = result
|
|
301
|
+
@namespace_definitions_version = @document.version
|
|
302
|
+
end
|
|
278
303
|
result
|
|
279
304
|
end
|
|
280
305
|
|
|
281
306
|
def namespaces
|
|
282
|
-
return @namespaces if
|
|
307
|
+
return @namespaces if memo_hit?(@namespaces_version)
|
|
283
308
|
scopes = {}
|
|
284
309
|
node = self
|
|
285
310
|
while node.is_a?(Leptris::XML::Element)
|
|
@@ -289,7 +314,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
289
314
|
end
|
|
290
315
|
node = node.parent
|
|
291
316
|
end
|
|
292
|
-
|
|
317
|
+
if @document
|
|
318
|
+
@namespaces = scopes
|
|
319
|
+
@namespaces_version = @document.version
|
|
320
|
+
end
|
|
293
321
|
scopes
|
|
294
322
|
end
|
|
295
323
|
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -101,13 +101,19 @@ 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
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
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
# Readonly is one-way, so caching TRUE is sound: once observed,
|
|
@@ -142,14 +148,17 @@ class Leptris::XML::Node
|
|
|
142
148
|
def children
|
|
143
149
|
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
144
150
|
# construction is paid once.
|
|
145
|
-
return @children if
|
|
151
|
+
return @children if memo_hit?(@children_version)
|
|
146
152
|
ensure_alive!
|
|
147
153
|
parent = as_element_or_self
|
|
148
154
|
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
149
155
|
Leptris::XML::Node.wrap(ptr, @document, parent: parent)
|
|
150
156
|
end
|
|
151
157
|
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
152
|
-
|
|
158
|
+
if @document
|
|
159
|
+
@children = result
|
|
160
|
+
@children_version = @document.version
|
|
161
|
+
end
|
|
153
162
|
result
|
|
154
163
|
end
|
|
155
164
|
|
|
@@ -170,14 +179,23 @@ class Leptris::XML::Node
|
|
|
170
179
|
alias_method :previous, :previous_sibling
|
|
171
180
|
|
|
172
181
|
def first_element_child
|
|
182
|
+
return @first_element_child if memo_hit?(@first_element_child_version)
|
|
173
183
|
ensure_alive!
|
|
174
184
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
185
|
+
result = nil
|
|
175
186
|
until ptr.nil? || ptr.null?
|
|
176
187
|
node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
177
|
-
|
|
188
|
+
if node.element?
|
|
189
|
+
result = node
|
|
190
|
+
break
|
|
191
|
+
end
|
|
178
192
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
179
193
|
end
|
|
180
|
-
|
|
194
|
+
if @document
|
|
195
|
+
@first_element_child = result
|
|
196
|
+
@first_element_child_version = @document.version
|
|
197
|
+
end
|
|
198
|
+
result
|
|
181
199
|
end
|
|
182
200
|
|
|
183
201
|
def last_element_child
|
|
@@ -185,9 +203,12 @@ class Leptris::XML::Node
|
|
|
185
203
|
end
|
|
186
204
|
|
|
187
205
|
def element_children
|
|
188
|
-
return @element_children if
|
|
206
|
+
return @element_children if memo_hit?(@element_children_version)
|
|
189
207
|
result = children.select(&:element?)
|
|
190
|
-
|
|
208
|
+
if @document
|
|
209
|
+
@element_children = result
|
|
210
|
+
@element_children_version = @document.version
|
|
211
|
+
end
|
|
191
212
|
result
|
|
192
213
|
end
|
|
193
214
|
alias_method :elements, :element_children
|
|
@@ -230,16 +251,19 @@ class Leptris::XML::Node
|
|
|
230
251
|
end
|
|
231
252
|
|
|
232
253
|
def path
|
|
233
|
-
return @path if
|
|
254
|
+
return @path if memo_hit?(@path_version)
|
|
234
255
|
ensure_alive!
|
|
235
256
|
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
|
|
236
257
|
result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
237
|
-
|
|
258
|
+
if @document
|
|
259
|
+
@path = result
|
|
260
|
+
@path_version = @document.version
|
|
261
|
+
end
|
|
238
262
|
result
|
|
239
263
|
end
|
|
240
264
|
|
|
241
265
|
def css_path
|
|
242
|
-
return @css_path if
|
|
266
|
+
return @css_path if memo_hit?(@css_path_version)
|
|
243
267
|
result =
|
|
244
268
|
if path.nil?
|
|
245
269
|
nil
|
|
@@ -249,7 +273,10 @@ class Leptris::XML::Node
|
|
|
249
273
|
part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
|
|
250
274
|
end.join(" > ")
|
|
251
275
|
end
|
|
252
|
-
|
|
276
|
+
if @document
|
|
277
|
+
@css_path = result
|
|
278
|
+
@css_path_version = @document.version
|
|
279
|
+
end
|
|
253
280
|
result
|
|
254
281
|
end
|
|
255
282
|
|
|
@@ -274,11 +301,15 @@ class Leptris::XML::Node
|
|
|
274
301
|
|
|
275
302
|
protected
|
|
276
303
|
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
|
|
304
|
+
# A memo is valid while the document's mutation version has not
|
|
305
|
+
# advanced since that memo was stored. Each memoized field carries
|
|
306
|
+
# its OWN stamp — a shared node-level stamp would let one field's
|
|
307
|
+
# recompute resurrect another field's stale memo. Readonly
|
|
308
|
+
# documents never advance the version, so their memos are forever
|
|
309
|
+
# valid (ADR 0003 semantics); writable documents gain memos
|
|
310
|
+
# between mutations.
|
|
311
|
+
def memo_hit?(stamp)
|
|
312
|
+
@document && stamp == @document.version
|
|
282
313
|
end
|
|
283
314
|
|
|
284
315
|
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.15
|
|
5
5
|
platform: ruby
|
|
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
|