leptris 1.9.1 → 1.9.3
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 +54 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/attr.rb +10 -8
- data/lib/leptris/xml/c_string_array.rb +1 -1
- data/lib/leptris/xml/cdata.rb +4 -1
- data/lib/leptris/xml/comment.rb +4 -1
- data/lib/leptris/xml/css_to_xpath.rb +12 -1
- data/lib/leptris/xml/document.rb +6 -1
- data/lib/leptris/xml/document_fragment.rb +5 -12
- data/lib/leptris/xml/element.rb +32 -11
- data/lib/leptris/xml/ffi.rb +133 -0
- data/lib/leptris/xml/node.rb +25 -15
- data/lib/leptris/xml/node_set.rb +16 -22
- data/lib/leptris/xml/processing_instruction.rb +8 -2
- data/lib/leptris/xml/pull.rb +6 -11
- data/lib/leptris/xml/searchable.rb +3 -12
- data/lib/leptris/xml/serialization.rb +4 -18
- data/lib/leptris/xml/text.rb +4 -1
- data/lib/leptris/xml/xpath.rb +1 -8
- 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: c3e961e97c5ce359508474f70efda320521debdd4eb5e851241a82a49ffe7512
|
|
4
|
+
data.tar.gz: 8fd855b22026aacac080e734658e115826b45e59dcbfee6bb01a0e2b024a8cd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1e58c09914cc12d9541410e1e5e46ff1c7135646bed22a46c6f796e41444ebdb0d039d1eabc1a5dc6790c6bcf2c3e1e31ae8fa6d8a33e25005138c5c0889fef
|
|
7
|
+
data.tar.gz: 42acbecab9c56654468540fa1cf7cda8dd666023a0d40b8f298ef2e18691d50e1e577393a0fb8bee36db27b743cdddf349db745e3b9c1bbb817d0a8681687a78
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ 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.3] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **UTF-8 at the FFI seam**: every string crossing the C boundary
|
|
13
|
+
(names, content, attribute values, paths, pull attributes, PI
|
|
14
|
+
data, error strings) now arrives as UTF-8 per the headers'
|
|
15
|
+
contract, instead of FFI's default ASCII-8BIT. Downstream string
|
|
16
|
+
comparisons, hash keys, and regexes stop paying compatibility
|
|
17
|
+
checks — and non-ASCII content can no longer raise
|
|
18
|
+
Encoding::CompatibilityError in consumer code.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Readonly read-path cache completed**: `namespace`,
|
|
23
|
+
`namespace_definitions`, `namespaces`, `keys`, `values`,
|
|
24
|
+
`attribute_nodes`, `element_children`, `path`, `css_path`,
|
|
25
|
+
Text/Comment/CDATA/PI `content` (and PI `name`), and
|
|
26
|
+
`Document#processing_instructions` now memoize under readonly —
|
|
27
|
+
the same cannot-go-stale invariant the first four readers already
|
|
28
|
+
had. Measured steady-state wins (readonly documents, 400-iteration
|
|
29
|
+
loops): namespace inspection 13.5× faster, attribute listing 3.2×,
|
|
30
|
+
text content 2×.
|
|
31
|
+
- **CSS translation cache**: `CssToXPath.convert` memoizes per
|
|
32
|
+
selector string (failures raise before caching); repeated
|
|
33
|
+
`css`/`at_css` vocabularies skip the regex cascade (~28% on the
|
|
34
|
+
selector loop benchmark).
|
|
35
|
+
- `Attr#to_xml` escapes the five XML entities in one gsub pass
|
|
36
|
+
instead of five chained passes; `Attr#prefix` slices instead of
|
|
37
|
+
split-and-discard.
|
|
38
|
+
|
|
39
|
+
## [1.9.2] - 2026-08-25
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
Architecture deepening — all C ABI and buffer knowledge now lives
|
|
44
|
+
behind the FFI seam (`lib/leptris/xml/ffi.rb`); no public API change.
|
|
45
|
+
|
|
46
|
+
- `PullEventStruct` (FFI::Struct) replaces the pull parser's
|
|
47
|
+
hand-rolled ABI offsets (`get_int(0)` / `get_pointer(8)` /
|
|
48
|
+
`get_pointer(16)`); struct-layout changes become a one-line
|
|
49
|
+
layout edit instead of silent offset drift.
|
|
50
|
+
- `FFI.with_ns_set(hash)` concentrates the namespace-binding
|
|
51
|
+
lifecycle (flatten → CStringArray wire format → build → yield →
|
|
52
|
+
free) that ad-hoc and compiled XPath each hand-wrote.
|
|
53
|
+
- `FFI.serialize_into_string` / `FFI.fetch_children` /
|
|
54
|
+
`FFI.fetch_result_nodes` / `FFI.parse_fragment_with_status` own
|
|
55
|
+
the size-query/allocate/fill/read buffer protocols; Serialization,
|
|
56
|
+
NodeSet and DocumentFragment now contain no `MemoryPointer` code.
|
|
57
|
+
- `Node#children` and `DocumentFragment#children` fetch all child
|
|
58
|
+
handles in one batched call (the libleptris 1.7.0 surface, now
|
|
59
|
+
attached) instead of the first_child + N next_sibling walk —
|
|
60
|
+
N+1 FFI round trips collapse to 2 dispatches plus N wraps.
|
|
61
|
+
|
|
8
62
|
## [1.9.1] - 2026-08-24
|
|
9
63
|
|
|
10
64
|
### Fixed
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/attr.rb
CHANGED
|
@@ -28,7 +28,8 @@ class Leptris::XML::Attr
|
|
|
28
28
|
# immutable — the same semantics as leptris_attribute_prefix, so
|
|
29
29
|
# no declaration lookup is involved.
|
|
30
30
|
def prefix
|
|
31
|
-
|
|
31
|
+
i = @name.index(":")
|
|
32
|
+
i ? @name[0, i] : nil
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# The attribute's namespace URI, resolved through the OWNING
|
|
@@ -52,14 +53,15 @@ class Leptris::XML::Attr
|
|
|
52
53
|
def to_str; @value; end
|
|
53
54
|
|
|
54
55
|
# Serialized attribute form: name="value" with the five XML special
|
|
55
|
-
# characters escaped in the value.
|
|
56
|
+
# characters escaped in the value — single pass, no chained gsubs.
|
|
57
|
+
ESCAPE_TABLE = {
|
|
58
|
+
"&" => "&", "<" => "<", ">" => ">",
|
|
59
|
+
'"' => """, "'" => "'",
|
|
60
|
+
}.freeze
|
|
61
|
+
private_constant :ESCAPE_TABLE
|
|
62
|
+
|
|
56
63
|
def to_xml
|
|
57
|
-
|
|
58
|
-
.gsub("<", "<")
|
|
59
|
-
.gsub(">", ">")
|
|
60
|
-
.gsub('"', """)
|
|
61
|
-
.gsub("'", "'")
|
|
62
|
-
"#{@name}=\"#{escaped}\""
|
|
64
|
+
"#{@name}=\"#{@value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)}\""
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def ==(other)
|
data/lib/leptris/xml/cdata.rb
CHANGED
|
@@ -4,7 +4,10 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
4
4
|
def name; "#cdata-section"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
|
|
7
|
+
return @content if readonly_cached?(:@content)
|
|
8
|
+
Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr).tap do |text|
|
|
9
|
+
@content = text if @document&.readonly?
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def content=(new_content)
|
data/lib/leptris/xml/comment.rb
CHANGED
|
@@ -4,7 +4,10 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
4
4
|
def name; "comment"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
|
|
7
|
+
return @content if readonly_cached?(:@content)
|
|
8
|
+
Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr).tap do |text|
|
|
9
|
+
@content = text if @document&.readonly?
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def content=(new_content)
|
|
@@ -35,8 +35,19 @@ module Leptris
|
|
|
35
35
|
|
|
36
36
|
module_function
|
|
37
37
|
|
|
38
|
+
# Translation is a pure function of the rule string, and real
|
|
39
|
+
# workloads repeat a small selector vocabulary in loops —
|
|
40
|
+
# memoize. Failed translations raise before caching.
|
|
41
|
+
CACHE = {}
|
|
42
|
+
private_constant :CACHE
|
|
43
|
+
|
|
38
44
|
def convert(rule)
|
|
39
|
-
rule.to_s
|
|
45
|
+
key = rule.to_s
|
|
46
|
+
CACHE.fetch(key) { CACHE[key] = convert_rule(key) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def convert_rule(rule)
|
|
50
|
+
rule.split(COMMA_SPLIT).map { |r| convert_one(r.strip) }.join(" | ")
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
def convert_one(rule)
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -247,11 +247,16 @@ class Leptris::XML::Document
|
|
|
247
247
|
# Document-level processing instructions (not tree nodes):
|
|
248
248
|
# an array of [target, data] pairs in document order.
|
|
249
249
|
def processing_instructions
|
|
250
|
+
if readonly? && instance_variable_defined?(:@processing_instructions)
|
|
251
|
+
return @processing_instructions
|
|
252
|
+
end
|
|
250
253
|
count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
|
|
251
|
-
count.times.map do |i|
|
|
254
|
+
result = count.times.map do |i|
|
|
252
255
|
[Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
|
|
253
256
|
Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
|
|
254
257
|
end
|
|
258
|
+
@processing_instructions = result if readonly?
|
|
259
|
+
result
|
|
255
260
|
end
|
|
256
261
|
|
|
257
262
|
# Append a document-level processing instruction. Returns self.
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ffi"
|
|
4
|
-
|
|
5
3
|
# Wraps the synthetic "#document-fragment" element returned by
|
|
6
4
|
# leptris_parse_fragment. Children of this fragment are the parsed nodes;
|
|
7
5
|
# the fragment itself isn't part of any document tree but borrows its
|
|
@@ -15,23 +13,18 @@ class Leptris::XML::DocumentFragment
|
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
def self.parse(xml, document)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr)
|
|
16
|
+
raw, status = Leptris::XML::FFI.parse_fragment_with_status(
|
|
17
|
+
xml.to_s, document.c_ptr)
|
|
21
18
|
if raw.null?
|
|
22
19
|
raise Leptris::XML::ParseError,
|
|
23
|
-
"leptris_parse_fragment failed (status=#{
|
|
20
|
+
"leptris_parse_fragment failed (status=#{status})"
|
|
24
21
|
end
|
|
25
22
|
new(document, raw)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def children
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
32
|
-
until ptr.nil? || ptr.null?
|
|
33
|
-
nodes << Leptris::XML::Node.wrap(ptr, @document)
|
|
34
|
-
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
26
|
+
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
27
|
+
Leptris::XML::Node.wrap(ptr, @document)
|
|
35
28
|
end
|
|
36
29
|
Leptris::XML::NodeSet.new(@document, nodes)
|
|
37
30
|
end
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -88,11 +88,17 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def keys
|
|
91
|
-
|
|
91
|
+
return @keys if readonly_cached?(:@keys)
|
|
92
|
+
result = each_attribute.to_a.map(&:name)
|
|
93
|
+
@keys = result if @document&.readonly?
|
|
94
|
+
result
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
def values
|
|
95
|
-
|
|
98
|
+
return @values if readonly_cached?(:@values)
|
|
99
|
+
result = each_attribute.to_a.map(&:value)
|
|
100
|
+
@values = result if @document&.readonly?
|
|
101
|
+
result
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
def attributes
|
|
@@ -104,7 +110,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
def attribute_nodes
|
|
107
|
-
|
|
113
|
+
return @attribute_nodes if readonly_cached?(:@attribute_nodes)
|
|
114
|
+
result = each_attribute.to_a
|
|
115
|
+
@attribute_nodes = result if @document&.readonly?
|
|
116
|
+
result
|
|
108
117
|
end
|
|
109
118
|
|
|
110
119
|
# The element's own namespace prefix (e.g. "foo" for <foo:child/>),
|
|
@@ -221,26 +230,37 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
221
230
|
alias_method :<<, :add_child
|
|
222
231
|
|
|
223
232
|
def namespace
|
|
233
|
+
return @namespace if readonly_cached?(:@namespace)
|
|
224
234
|
uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
result =
|
|
236
|
+
if uri.nil? || uri.empty?
|
|
237
|
+
nil
|
|
238
|
+
else
|
|
239
|
+
# The resolved namespace is reached via this element's own
|
|
240
|
+
# prefix, so carry it through: consumers that distinguish
|
|
241
|
+
# {"p" => "urn:p"} from the default {"nil => "urn:p"} need it.
|
|
242
|
+
prefix = Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
|
|
243
|
+
prefix = nil if prefix.nil? || prefix.empty?
|
|
244
|
+
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
245
|
+
end
|
|
246
|
+
@namespace = result if @document&.readonly?
|
|
247
|
+
result
|
|
232
248
|
end
|
|
233
249
|
|
|
234
250
|
def namespace_definitions
|
|
251
|
+
return @namespace_definitions if readonly_cached?(:@namespace_definitions)
|
|
235
252
|
count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
|
|
236
|
-
count.times.map do |i|
|
|
253
|
+
result = count.times.map do |i|
|
|
237
254
|
prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
|
|
238
255
|
uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
|
|
239
256
|
Leptris::XML::Namespace.new(self, uri, prefix: prefix)
|
|
240
257
|
end
|
|
258
|
+
@namespace_definitions = result if @document&.readonly?
|
|
259
|
+
result
|
|
241
260
|
end
|
|
242
261
|
|
|
243
262
|
def namespaces
|
|
263
|
+
return @namespaces if readonly_cached?(:@namespaces)
|
|
244
264
|
scopes = {}
|
|
245
265
|
node = self
|
|
246
266
|
while node.is_a?(Leptris::XML::Element)
|
|
@@ -250,6 +270,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
250
270
|
end
|
|
251
271
|
node = node.parent
|
|
252
272
|
end
|
|
273
|
+
@namespaces = scopes if @document&.readonly?
|
|
253
274
|
scopes
|
|
254
275
|
end
|
|
255
276
|
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -80,6 +80,17 @@ module Leptris
|
|
|
80
80
|
:recover, :int
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# struct LeptrisPullEvent (leptris/sax.h): FFI::Struct derives
|
|
84
|
+
# the field offsets from the layout, so ABI changes are a
|
|
85
|
+
# one-line edit here instead of silent offset drift in callers.
|
|
86
|
+
class PullEventStruct < ::FFI::Struct
|
|
87
|
+
layout \
|
|
88
|
+
:type, :int,
|
|
89
|
+
:name, :pointer,
|
|
90
|
+
:text, :pointer,
|
|
91
|
+
:text_len, :size_t
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
attach_function :leptris_version, [], :string
|
|
84
95
|
attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
|
|
85
96
|
|
|
@@ -158,6 +169,10 @@ module Leptris
|
|
|
158
169
|
[:leptris_node_ref], :leptris_node_ref
|
|
159
170
|
attach_function :leptris_node_child_count,
|
|
160
171
|
[:leptris_node_ref], :size_t
|
|
172
|
+
# libleptris 1.7.0: all-kind child batch — out_nodes=NULL is a
|
|
173
|
+
# count-only query returning the TOTAL across every child kind.
|
|
174
|
+
attach_function :leptris_node_children,
|
|
175
|
+
[:leptris_node_ref, :pointer, :size_t], :size_t
|
|
161
176
|
attach_function :leptris_node_as_element,
|
|
162
177
|
[:leptris_node_ref], :leptris_element
|
|
163
178
|
attach_function :leptris_element_as_node,
|
|
@@ -600,11 +615,45 @@ module Leptris
|
|
|
600
615
|
LEPTRIS_PARSE_DEFAULT = 0
|
|
601
616
|
LEPTRIS_PARSE_DROP_WS_TEXT = 1
|
|
602
617
|
|
|
618
|
+
# The headers' contract is UTF-8 for every C string, but FFI's
|
|
619
|
+
# read_string hands back ASCII-8BIT — the platform default
|
|
620
|
+
# leaks through the seam. Every string-returning attached
|
|
621
|
+
# function this binding CALLS is wrapped here so its result
|
|
622
|
+
# arrives as UTF-8; mirror-only attachments stay raw (add the
|
|
623
|
+
# name here when a call site starts using its string).
|
|
624
|
+
UTF8_RETURNS = %i[
|
|
625
|
+
leptris_version
|
|
626
|
+
leptris_status_string leptris_last_error
|
|
627
|
+
leptris_document_last_error leptris_document_encoding
|
|
628
|
+
leptris_document_pi_target leptris_document_pi_data
|
|
629
|
+
leptris_element_name leptris_element_text leptris_element_prefix
|
|
630
|
+
leptris_element_namespace
|
|
631
|
+
leptris_element_attribute leptris_element_attribute_ns
|
|
632
|
+
leptris_attribute_get_name leptris_attribute_get_value
|
|
633
|
+
leptris_attribute_namespace_uri
|
|
634
|
+
leptris_element_namespace_decl_prefix
|
|
635
|
+
leptris_element_namespace_decl_uri
|
|
636
|
+
leptris_text_node_get_content leptris_comment_node_get_content
|
|
637
|
+
leptris_cdata_node_get_content
|
|
638
|
+
leptris_pi_node_get_target leptris_pi_node_get_data
|
|
639
|
+
leptris_pull_attr_name leptris_pull_attr_value
|
|
640
|
+
].freeze
|
|
641
|
+
private_constant :UTF8_RETURNS
|
|
642
|
+
|
|
643
|
+
UTF8_RETURNS.each do |name|
|
|
644
|
+
raw = method(name)
|
|
645
|
+
define_singleton_method(name) do |*args|
|
|
646
|
+
str = raw.call(*args)
|
|
647
|
+
str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
603
651
|
# Reads an libleptris-owned char* result and frees it as one unit,
|
|
604
652
|
# so a call site can neither leak nor double-free.
|
|
605
653
|
def self.read_owned_string(ptr)
|
|
606
654
|
return "" if ptr.nil? || ptr.null?
|
|
607
655
|
ptr.read_string.tap { leptris_free_string(ptr) }
|
|
656
|
+
.force_encoding(Encoding::UTF_8)
|
|
608
657
|
end
|
|
609
658
|
|
|
610
659
|
# Single status seam: turns a C status code into a Ruby error,
|
|
@@ -623,6 +672,90 @@ module Leptris
|
|
|
623
672
|
detail = leptris_last_error.to_s
|
|
624
673
|
detail.empty? ? base : "#{base} (#{detail})"
|
|
625
674
|
end
|
|
675
|
+
|
|
676
|
+
# Namespace-binding lifecycle, written once: flatten the
|
|
677
|
+
# prefix/URI hash into the alternating CStringArray wire
|
|
678
|
+
# format, build the caller-owned set, yield it, free it under
|
|
679
|
+
# all outcomes. Every eval variant uses this; none of them
|
|
680
|
+
# knows how a set is born or dies.
|
|
681
|
+
def self.with_ns_set(hash)
|
|
682
|
+
flat = hash.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
|
|
683
|
+
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
684
|
+
set = leptris_xpath_ns_set_new_from_pairs(buffer, flat.length / 2)
|
|
685
|
+
if set.null?
|
|
686
|
+
raise Leptris::XML::Error,
|
|
687
|
+
"leptris_xpath_ns_set_new_from_pairs failed"
|
|
688
|
+
end
|
|
689
|
+
begin
|
|
690
|
+
yield set
|
|
691
|
+
ensure
|
|
692
|
+
leptris_xpath_ns_set_free(set)
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Caller-buffer serialization cycle: size query (buf=NULL),
|
|
697
|
+
# allocate exactly, fill, read. Since libleptris 1.9.0 the
|
|
698
|
+
# pair reuses one serialization through the per-document
|
|
699
|
+
# cache. +ffi_function+ is leptris_document_serialize_into or
|
|
700
|
+
# leptris_element_serialize_into. The buffer holds the
|
|
701
|
+
# serialization + NUL and XML output is NUL-free, so the
|
|
702
|
+
# bounded read is exact.
|
|
703
|
+
def self.serialize_into_string(ffi_function, c_ptr, options)
|
|
704
|
+
need = ffi_function.call(c_ptr, nil, 0, nil, options)
|
|
705
|
+
return "" if need.zero?
|
|
706
|
+
buffer = ::FFI::MemoryPointer.new(:char, need)
|
|
707
|
+
begin
|
|
708
|
+
ffi_function.call(c_ptr, buffer, need, nil, options)
|
|
709
|
+
buffer.read_string
|
|
710
|
+
ensure
|
|
711
|
+
buffer.free
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# All-kind child handle batch (libleptris 1.7.0): count-only
|
|
716
|
+
# query sizes the buffer, one fetch copies every child kind,
|
|
717
|
+
# one bulk read materializes the pointers.
|
|
718
|
+
def self.fetch_children(c_ptr)
|
|
719
|
+
count = leptris_node_children(c_ptr, nil, 0)
|
|
720
|
+
return [] if count.zero?
|
|
721
|
+
with_pointer_buffer(count) do |buffer|
|
|
722
|
+
copied = leptris_node_children(c_ptr, buffer, count)
|
|
723
|
+
buffer.get_array_of_pointer(0, copied)
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
728
|
+
# copies all node kinds into a caller buffer and hands back
|
|
729
|
+
# the pointer array.
|
|
730
|
+
def self.fetch_result_nodes(result_ptr, count)
|
|
731
|
+
return [] if count.zero?
|
|
732
|
+
with_pointer_buffer(count) do |buffer|
|
|
733
|
+
copied = leptris_xpath_result_get_nodes_ex(
|
|
734
|
+
result_ptr, buffer, nil, count)
|
|
735
|
+
buffer.get_array_of_pointer(0, copied)
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Fragment parse with the status out-param read: returns
|
|
740
|
+
# [document-or-fragment pointer, status].
|
|
741
|
+
def self.parse_fragment_with_status(xml, document_ptr)
|
|
742
|
+
status = ::FFI::MemoryPointer.new(:int)
|
|
743
|
+
raw = leptris_parse_fragment(
|
|
744
|
+
xml, xml.bytesize, document_ptr, status)
|
|
745
|
+
[raw, status.read_int]
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# The one buffer-allocation point for handle-array fetches:
|
|
749
|
+
# everything MemoryPointer-related above this line is either
|
|
750
|
+
# an attached C call or another seam helper.
|
|
751
|
+
def self.with_pointer_buffer(count)
|
|
752
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, count)
|
|
753
|
+
begin
|
|
754
|
+
yield buffer
|
|
755
|
+
ensure
|
|
756
|
+
buffer.free
|
|
757
|
+
end
|
|
758
|
+
end
|
|
626
759
|
end
|
|
627
760
|
end
|
|
628
761
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -107,14 +107,12 @@ class Leptris::XML::Node
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def children
|
|
110
|
-
# Immutable in readonly mode: the
|
|
111
|
-
#
|
|
110
|
+
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
111
|
+
# construction is paid once.
|
|
112
112
|
return @children if readonly_cached?(:@children)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
117
|
-
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
113
|
+
parent = as_element_or_self
|
|
114
|
+
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
115
|
+
Leptris::XML::Node.wrap(ptr, @document, parent: parent)
|
|
118
116
|
end
|
|
119
117
|
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
120
118
|
@children = result if @document&.readonly?
|
|
@@ -150,7 +148,10 @@ class Leptris::XML::Node
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
def element_children
|
|
153
|
-
|
|
151
|
+
return @element_children if readonly_cached?(:@element_children)
|
|
152
|
+
result = children.select(&:element?)
|
|
153
|
+
@element_children = result if @document&.readonly?
|
|
154
|
+
result
|
|
154
155
|
end
|
|
155
156
|
alias_method :elements, :element_children
|
|
156
157
|
|
|
@@ -197,17 +198,26 @@ class Leptris::XML::Node
|
|
|
197
198
|
end
|
|
198
199
|
|
|
199
200
|
def path
|
|
201
|
+
return @path if readonly_cached?(:@path)
|
|
200
202
|
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
204
|
+
@path = result if @document&.readonly?
|
|
205
|
+
result
|
|
203
206
|
end
|
|
204
207
|
|
|
205
208
|
def css_path
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
return @css_path if readonly_cached?(:@css_path)
|
|
210
|
+
result =
|
|
211
|
+
if path.nil?
|
|
212
|
+
nil
|
|
213
|
+
else
|
|
214
|
+
path.split("/").filter_map do |part|
|
|
215
|
+
next nil if part.empty?
|
|
216
|
+
part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
|
|
217
|
+
end.join(" > ")
|
|
218
|
+
end
|
|
219
|
+
@css_path = result if @document&.readonly?
|
|
220
|
+
result
|
|
211
221
|
end
|
|
212
222
|
|
|
213
223
|
def dup
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -63,30 +63,24 @@ class Leptris::XML::NodeSet
|
|
|
63
63
|
def each
|
|
64
64
|
return enum_for(:each) unless block_given?
|
|
65
65
|
if @result_ptr
|
|
66
|
-
# Batch-fetch all node pointers
|
|
67
|
-
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
68
|
-
# just elements) and wrap each. Wrappers are cached
|
|
69
|
-
# via Node.wrap, so re-iteration of the same
|
|
66
|
+
# Batch-fetch all node pointers through the FFI seam
|
|
67
|
+
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
68
|
+
# not just elements) and wrap each. Wrappers are cached
|
|
69
|
+
# per-Document via Node.wrap, so re-iteration of the same
|
|
70
|
+
# NodeSet hits it.
|
|
70
71
|
n = length
|
|
71
72
|
if n > 0
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
(copied...n).each do |i|
|
|
84
|
-
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
85
|
-
next if ptr.null?
|
|
86
|
-
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
87
|
-
end
|
|
88
|
-
ensure
|
|
89
|
-
buf.free
|
|
73
|
+
pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
74
|
+
pointers.each do |ptr|
|
|
75
|
+
next if ptr.null?
|
|
76
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
77
|
+
end
|
|
78
|
+
# The batch accessor under-copies mixed-kind results (upstream
|
|
79
|
+
# leptris#477); fall back to per-index fetch for the remainder.
|
|
80
|
+
(pointers.length...n).each do |i|
|
|
81
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
82
|
+
next if ptr.null?
|
|
83
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
90
84
|
end
|
|
91
85
|
end
|
|
92
86
|
else
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
4
4
|
def name
|
|
5
|
-
|
|
5
|
+
return @name if readonly_cached?(:@name)
|
|
6
|
+
Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr).tap do |target|
|
|
7
|
+
@name = target if @document&.readonly?
|
|
8
|
+
end
|
|
6
9
|
end
|
|
7
10
|
alias_method :target, :name
|
|
8
11
|
|
|
9
12
|
def content
|
|
10
|
-
|
|
13
|
+
return @content if readonly_cached?(:@content)
|
|
14
|
+
Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s.tap do |data|
|
|
15
|
+
@content = data if @document&.readonly?
|
|
16
|
+
end
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def target=(new_target)
|
data/lib/leptris/xml/pull.rb
CHANGED
|
@@ -67,23 +67,18 @@ module Leptris::XML::Pull
|
|
|
67
67
|
def next_event
|
|
68
68
|
raw = Leptris::XML::FFI.leptris_pull_next(@handle)
|
|
69
69
|
return nil if raw.null?
|
|
70
|
-
|
|
71
|
-
type = TYPES[
|
|
72
|
-
name =
|
|
73
|
-
|
|
74
|
-
text = text_ptr.null? ? nil : text_ptr.read_string
|
|
70
|
+
event = Leptris::XML::FFI::PullEventStruct.new(raw)
|
|
71
|
+
type = TYPES[event[:type]]
|
|
72
|
+
name = read_owned(event[:name])
|
|
73
|
+
text = read_owned(event[:text])
|
|
75
74
|
attrs = type == :start_element ? capture_attrs : nil
|
|
76
75
|
Event.new(type: type, name: name, text: text, attrs: attrs)
|
|
77
76
|
end
|
|
78
77
|
|
|
79
78
|
private
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# 4-byte enum on 64-bit ABIs (name at 8, text at 16).
|
|
84
|
-
def read_string_at(raw, offset)
|
|
85
|
-
ptr = raw.get_pointer(offset)
|
|
86
|
-
ptr.null? ? nil : ptr.read_string
|
|
80
|
+
def read_owned(ptr)
|
|
81
|
+
ptr.null? ? nil : ptr.read_string.force_encoding(Encoding::UTF_8)
|
|
87
82
|
end
|
|
88
83
|
|
|
89
84
|
def capture_attrs
|
|
@@ -55,20 +55,11 @@ module Leptris::XML::Searchable
|
|
|
55
55
|
|
|
56
56
|
protected
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# the same wire format CStringArray owns), evaluates via
|
|
61
|
-
# leptris_xpath_eval_ns, and frees the set regardless of outcome.
|
|
58
|
+
# Evaluates against a caller-owned namespace binding set; the
|
|
59
|
+
# build/teardown lifecycle lives at the FFI seam (FFI.with_ns_set).
|
|
62
60
|
def xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
63
|
-
|
|
64
|
-
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
65
|
-
set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
|
|
66
|
-
buffer, flat.length / 2)
|
|
67
|
-
raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
|
|
68
|
-
begin
|
|
61
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
69
62
|
Leptris::XML::FFI.leptris_xpath_eval_ns(doc_ptr, context_ptr, expr, set)
|
|
70
|
-
ensure
|
|
71
|
-
Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
|
|
72
63
|
end
|
|
73
64
|
end
|
|
74
65
|
|
|
@@ -18,12 +18,9 @@ module Leptris::XML::Serialization
|
|
|
18
18
|
end
|
|
19
19
|
private_constant :DEFAULT_OPTIONS
|
|
20
20
|
|
|
21
|
-
# +ffi_function+ is a bound _serialize_into function
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# pair reuses a single serialization through a per-document cache
|
|
25
|
-
# (leptris#541) — no C-side result-string allocation, and repeated
|
|
26
|
-
# serializations of an unmutated document skip the serialize pass.
|
|
21
|
+
# +ffi_function+ is a bound _serialize_into function. The buffer
|
|
22
|
+
# cycle lives at the FFI seam (FFI.serialize_into_string); this
|
|
23
|
+
# module owns only options selection and construction.
|
|
27
24
|
def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
|
|
28
25
|
opts =
|
|
29
26
|
if indent.to_i.zero? && !no_decl && encoding.nil?
|
|
@@ -33,18 +30,7 @@ module Leptris::XML::Serialization
|
|
|
33
30
|
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
34
31
|
opts
|
|
35
32
|
end
|
|
36
|
-
|
|
37
|
-
return "" if need.zero?
|
|
38
|
-
buf = ::FFI::MemoryPointer.new(:char, need)
|
|
39
|
-
begin
|
|
40
|
-
ffi_function.call(c_ptr, buf, need, nil, opts.pointer)
|
|
41
|
-
# The buffer holds exactly the serialization + NUL; XML output
|
|
42
|
-
# is NUL-free, so a bounded read_string is exact. (Avoids the
|
|
43
|
-
# out_len param — no portable size_t read exists on Pointer.)
|
|
44
|
-
buf.read_string
|
|
45
|
-
ensure
|
|
46
|
-
buf.free
|
|
47
|
-
end
|
|
33
|
+
Leptris::XML::FFI.serialize_into_string(ffi_function, c_ptr, opts.pointer)
|
|
48
34
|
end
|
|
49
35
|
|
|
50
36
|
# +ffi_function+ is a bound FFI function taking
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -4,7 +4,10 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
4
4
|
def name; "text"; end
|
|
5
5
|
|
|
6
6
|
def content
|
|
7
|
-
|
|
7
|
+
return @content if readonly_cached?(:@content)
|
|
8
|
+
Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr).tap do |text|
|
|
9
|
+
@content = text if @document&.readonly?
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def content=(new_content)
|
data/lib/leptris/xml/xpath.rb
CHANGED
|
@@ -40,16 +40,9 @@ class Leptris::XML::XPath
|
|
|
40
40
|
document = context.document
|
|
41
41
|
result_ptr =
|
|
42
42
|
if ns && !ns.empty?
|
|
43
|
-
|
|
44
|
-
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
45
|
-
set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
|
|
46
|
-
buffer, flat.length / 2)
|
|
47
|
-
raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
|
|
48
|
-
begin
|
|
43
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
49
44
|
Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
|
|
50
45
|
@handle, document.c_ptr, context_ptr(context), set)
|
|
51
|
-
ensure
|
|
52
|
-
Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
|
|
53
46
|
end
|
|
54
47
|
else
|
|
55
48
|
Leptris::XML::FFI.leptris_xpath_compiled_eval(
|
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.3
|
|
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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|