leptris 1.9.15 → 1.9.17
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 +30 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +19 -6
- data/lib/leptris/xml/node.rb +5 -2
- data/lib/leptris/xml/node_set.rb +3 -3
- data/lib/leptris/xml/searchable.rb +42 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 790fde1b932902a17438b2abed525393ebb61464a61ff35081384409277a42cc
|
|
4
|
+
data.tar.gz: 356b2a9a9426c03a25c42577952f3634127162e449e3540816b99a8bf6cd354f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fdfd7ed098ae1501040d5f82808c57813f5f2f8dec606d6ac966bd916cff635dd510ebf68e235a4a3e9c0da6a5a10cc9765ffe20eb06a476876dc2cf6dbc2fc
|
|
7
|
+
data.tar.gz: 26142e64a808758e72332fa8aaef029b7fee19c82c3f634f6f0ec9ba7afe7990261f958173a124f2f89ced5270c253f53256f4b12eafc99235a066e38c5b7f3a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ 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.17] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Element-hinted batch materialization**: the XPath result-set
|
|
13
|
+
batch (`leptris_xpath_result_get_nodes_ex`) fills an out_kinds
|
|
14
|
+
array the binding discarded; `Node.wrap` now accepts a known
|
|
15
|
+
`node_type:` so element entries skip the per-node get_type
|
|
16
|
+
dispatch. ELEMENT is the only hintable value — XPath's data
|
|
17
|
+
model reports CDATA as TEXT, so a TEXT hint cannot distinguish
|
|
18
|
+
Text from CDATA (different content getters); comments/CDATA/PIs
|
|
19
|
+
keep the get_type fallback. Cold 500-element materialization:
|
|
20
|
+
0.131 s -> 0.110 s (~15%). Upstream note: a full-type out_kinds
|
|
21
|
+
(or kinds on `leptris_node_children`) belongs to the #560/#562
|
|
22
|
+
ask family.
|
|
23
|
+
|
|
24
|
+
## [1.9.16] - 2026-08-27
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **at_xpath / at_css take the single-node path**: a new
|
|
29
|
+
`wrap_xpath_first_result` seam answers nodeset results via
|
|
30
|
+
get_node(0) + wrap + free — no NodeSet container, no AutoPointer,
|
|
31
|
+
one fewer FFI than xpath().first; scalar results keep full-wrapper
|
|
32
|
+
semantics. Measured on the canonical scraper loop: at_xpath 0.018 s
|
|
33
|
+
-> 0.005 s (3.6x — the per-iteration container churn was GC
|
|
34
|
+
pressure the old path paid twice), at_css ~24%. Equivalence with
|
|
35
|
+
xpath().first is spec-pinned across nodeset/empty/scalar shapes.
|
|
36
|
+
Also drops a vestigial `.send(:from_result)` on a public factory.
|
|
37
|
+
|
|
8
38
|
## [1.9.15] - 2026-08-27
|
|
9
39
|
|
|
10
40
|
### Changed
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -747,14 +747,27 @@ module Leptris
|
|
|
747
747
|
end
|
|
748
748
|
|
|
749
749
|
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
750
|
-
#
|
|
751
|
-
# the
|
|
750
|
+
# the call fills out_kinds in the 4-value XPATH_NODE space
|
|
751
|
+
# (ELEMENT/ATTRIBUTE/TEXT/OTHER). ELEMENT is the only value
|
|
752
|
+
# that maps unambiguously — XPath's data model reports CDATA
|
|
753
|
+
# as TEXT, so a TEXT hint cannot distinguish Text from CDATA
|
|
754
|
+
# (different content getters) — everything else falls back to
|
|
755
|
+
# get_type in the wrapper. Returns [pointers, element_hints].
|
|
756
|
+
XPATH_KIND_HINT = { XPATH_NODE_ELEMENT => NODE_ELEMENT }.freeze
|
|
757
|
+
|
|
752
758
|
def self.fetch_result_nodes(result_ptr, count)
|
|
753
|
-
return [] if count.zero?
|
|
759
|
+
return [[], nil] if count.zero?
|
|
754
760
|
with_pointer_buffer(count) do |buffer|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
761
|
+
kinds = ::FFI::MemoryPointer.new(:int, count)
|
|
762
|
+
begin
|
|
763
|
+
copied = leptris_xpath_result_get_nodes_ex(
|
|
764
|
+
result_ptr, buffer, kinds, count)
|
|
765
|
+
hints = kinds.get_array_of_int(0, copied)
|
|
766
|
+
.map { |k| XPATH_KIND_HINT[k] }
|
|
767
|
+
[buffer.get_array_of_pointer(0, copied), hints]
|
|
768
|
+
ensure
|
|
769
|
+
kinds.free
|
|
770
|
+
end
|
|
758
771
|
end
|
|
759
772
|
end
|
|
760
773
|
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -12,7 +12,10 @@ class Leptris::XML::Node
|
|
|
12
12
|
@node_type = node_type
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# node_type: callers holding a batch-fetched kind (the XPath
|
|
16
|
+
# result-set batch fills out_kinds) pass it so the wrap skips the
|
|
17
|
+
# get_type dispatch; nil (the default) dispatches as before.
|
|
18
|
+
def self.wrap(c_ptr, document, parent: nil, node_type: nil)
|
|
16
19
|
# Per-document weak-ref cache. Returns the existing wrapper when the
|
|
17
20
|
# same c_ptr is wrapped twice (common in children/sibling walks,
|
|
18
21
|
# repeated xpath queries, traverse-then-access patterns). The cache
|
|
@@ -21,7 +24,7 @@ class Leptris::XML::Node
|
|
|
21
24
|
return cached
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
node_type
|
|
27
|
+
node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
25
28
|
node =
|
|
26
29
|
case node_type
|
|
27
30
|
when Leptris::XML::FFI::NODE_ELEMENT
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -81,11 +81,11 @@ class Leptris::XML::NodeSet
|
|
|
81
81
|
# not just elements); the batch accessor under-copies
|
|
82
82
|
# mixed-kind results (upstream leptris#477), so per-index
|
|
83
83
|
# fetch covers the remainder.
|
|
84
|
-
pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
84
|
+
pointers, kinds = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
85
85
|
nodes = []
|
|
86
|
-
pointers.
|
|
86
|
+
pointers.each_with_index do |ptr, i|
|
|
87
87
|
next if ptr.null?
|
|
88
|
-
nodes << Leptris::XML::Node.wrap(ptr, @document)
|
|
88
|
+
nodes << Leptris::XML::Node.wrap(ptr, @document, node_type: kinds[i])
|
|
89
89
|
end
|
|
90
90
|
(pointers.length...n).each do |i|
|
|
91
91
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
@@ -24,8 +24,24 @@ module Leptris::XML::Searchable
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def at_xpath(*paths)
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
handler, ns, _vars = parse_search_args(paths)
|
|
28
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
29
|
+
expr = paths.join(" | ")
|
|
30
|
+
|
|
31
|
+
doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
|
|
32
|
+
context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
|
|
33
|
+
|
|
34
|
+
result_ptr =
|
|
35
|
+
if ns && !ns.empty?
|
|
36
|
+
xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
37
|
+
else
|
|
38
|
+
Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
|
|
39
|
+
end
|
|
40
|
+
if result_ptr.null?
|
|
41
|
+
raise Leptris::XML::XPathError,
|
|
42
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
43
|
+
end
|
|
44
|
+
Leptris::XML::Searchable.wrap_xpath_first_result(document, result_ptr)
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
def search(*args)
|
|
@@ -53,8 +69,13 @@ module Leptris::XML::Searchable
|
|
|
53
69
|
end
|
|
54
70
|
|
|
55
71
|
def at_css(*args)
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
handler, ns, _ = parse_search_args(args)
|
|
73
|
+
raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
|
|
74
|
+
raise ArgumentError, "custom CSS handlers not supported" if handler
|
|
75
|
+
prefix = is_a?(Leptris::XML::Document) ? "//" : ".//"
|
|
76
|
+
expr = args.map { |r| Leptris::XML::CssToXPath.convert(r, prefix: prefix) }
|
|
77
|
+
.join(" | ")
|
|
78
|
+
at_xpath(expr)
|
|
58
79
|
end
|
|
59
80
|
|
|
60
81
|
protected
|
|
@@ -86,6 +107,22 @@ module Leptris::XML::Searchable
|
|
|
86
107
|
%r{\A(\./|/|\.\.|\.)}.match?(str)
|
|
87
108
|
end
|
|
88
109
|
|
|
110
|
+
# Single-node seam beside wrap_xpath_result: a nodeset answers
|
|
111
|
+
# via result_get_node(0) + wrap + free — no NodeSet container, no
|
|
112
|
+
# AutoPointer, one fewer FFI than xpath().first; scalars keep the
|
|
113
|
+
# full-wrapper semantics.
|
|
114
|
+
def self.wrap_xpath_first_result(document, result_ptr)
|
|
115
|
+
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
116
|
+
if type == Leptris::XML::FFI::XPATH_NODESET
|
|
117
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(result_ptr, 0)
|
|
118
|
+
node = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, document)
|
|
119
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
120
|
+
node
|
|
121
|
+
else
|
|
122
|
+
wrap_xpath_result(document, result_ptr)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
89
126
|
# Shared by Searchable#xpath and Leptris::XML::XPath (compiled
|
|
90
127
|
# expressions): wraps a raw XPathResult pointer into the
|
|
91
128
|
# Ruby-typed result and frees the C handle.
|
|
@@ -93,7 +130,7 @@ module Leptris::XML::Searchable
|
|
|
93
130
|
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
94
131
|
case type
|
|
95
132
|
when Leptris::XML::FFI::XPATH_NODESET
|
|
96
|
-
Leptris::XML::NodeSet.
|
|
133
|
+
Leptris::XML::NodeSet.from_result(document, result_ptr)
|
|
97
134
|
when Leptris::XML::FFI::XPATH_BOOLEAN
|
|
98
135
|
v = Leptris::XML::FFI.leptris_xpath_result_boolean(result_ptr) != 0
|
|
99
136
|
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|