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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1b73d7abca12056f163aed086825140416384075955e593f11191156db475fc
4
- data.tar.gz: ead9075339cfd0736ca79cbede18612702f4632c95d9d90a512f67b3e4c32ec7
3
+ metadata.gz: 790fde1b932902a17438b2abed525393ebb61464a61ff35081384409277a42cc
4
+ data.tar.gz: 356b2a9a9426c03a25c42577952f3634127162e449e3540816b99a8bf6cd354f
5
5
  SHA512:
6
- metadata.gz: 22ec81a95a094913096d9c0749c42a64274f7af9225c4128b51529c8160f6480d3f23476b548f7c766202be9744dee9a830eaa715dbeea77a42e4d374f61b6e5
7
- data.tar.gz: 5e5408837f3ca2b4b54e4069a36798000b67d87fee9ed7021927b4e6c8b95d6a04a9f9d770fcd786763569d700182f87fc5b7df54c14462391c0420f69d108ed
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.15"
4
+ VERSION = "1.9.17"
5
5
  end
@@ -747,14 +747,27 @@ module Leptris
747
747
  end
748
748
 
749
749
  # XPath result-set batch (leptris_xpath_result_get_nodes_ex):
750
- # copies all node kinds into a caller buffer and hands back
751
- # the pointer array.
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
- copied = leptris_xpath_result_get_nodes_ex(
756
- result_ptr, buffer, nil, count)
757
- buffer.get_array_of_pointer(0, copied)
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
 
@@ -12,7 +12,10 @@ class Leptris::XML::Node
12
12
  @node_type = node_type
13
13
  end
14
14
 
15
- def self.wrap(c_ptr, document, parent: nil)
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 = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
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
@@ -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.each do |ptr|
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
- result = xpath(*paths)
28
- result.is_a?(Leptris::XML::NodeSet) ? result.first : result
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
- result = css(*args)
57
- result.is_a?(Leptris::XML::NodeSet) ? result.first : result
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.send(:from_result, document, result_ptr)
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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.15
4
+ version: 1.9.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.