leptris 1.9.25 → 1.9.26
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 +38 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +20 -0
- data/lib/leptris/xml/ffi.rb +31 -0
- data/lib/leptris/xml/node_set.rb +13 -14
- data/lib/leptris/xml/pull.rb +65 -10
- 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: 4c1650cd7d5f1be133b6c265c5ea6bfd646265ac443a86361bb30593dd0d255a
|
|
4
|
+
data.tar.gz: babd8da0971c8f6820f2692e7283d249cea85455f84620a73e86fe488caa1a18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62d2b51adc4bd84f8a3f6c0ebc749ad328b1ceed1d9a6502234819e0c5f3bf054ac758f008beba3692f2781981d7a4de23dc0e0d6f44462f2e3d385c2eb468e8
|
|
7
|
+
data.tar.gz: 22a1369e2755c016019691507c2e867e792352fcd999be595c9fd485934f6170f44ce4320366487b1f0d30fd314b6db4941773663a6722b542af501ebc131bac
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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.26] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **libleptris 1.9.7 lockstep** (upstream #602/#604): Rakefile pin
|
|
13
|
+
bumped; symbol audit 247/247 with the five new functions.
|
|
14
|
+
- **`Pull::Parser#each_batch(max = 256)`** — the #589 batch
|
|
15
|
+
transport: ONE C call stages up to max events (scratch-staged,
|
|
16
|
+
layout-offset drain, no per-event dispatch). Streaming big.xml
|
|
17
|
+
(250k events): 408 ms -> **210 ms per parse (-48%)**; the cursor
|
|
18
|
+
path also drops to 347 ms (Events are now constructed
|
|
19
|
+
positionally — the keyword-init Struct cost ~220 ns/event; a
|
|
20
|
+
minor API change: `Pull::Event` no longer takes keyword args,
|
|
21
|
+
readers unchanged). Per the engine's attr-mirror protocol,
|
|
22
|
+
attributes are captured for each batch's LAST start_element —
|
|
23
|
+
use `#each` when every start's attrs must be present. Attr
|
|
24
|
+
capture on both paths is now one count query + one flat copy
|
|
25
|
+
(`leptris_pull_attrs`) instead of 2N per-index dispatches.
|
|
26
|
+
- **`Document#node` / `Document#children`** (#580, our upstream
|
|
27
|
+
ask): document-level PIs/comments are tree children behind a
|
|
28
|
+
stable singleton navigation head — `children` is
|
|
29
|
+
[prolog…, root, epilog…] in document order (the libxml2/Nokogiri
|
|
30
|
+
model), and `/comment()` / `//processing-instruction()` see the
|
|
31
|
+
document-level nodes. The dedicated memoized
|
|
32
|
+
`#processing_instructions` / `#comments` readers are unchanged.
|
|
33
|
+
- **`NodeSet#xpath` through the union entry** (`leptris_xpath_
|
|
34
|
+
eval_nodeset`, #589): one C call for N contexts — results
|
|
35
|
+
de-duplicated and document-ordered, which the per-member Ruby
|
|
36
|
+
loop it replaces could not guarantee.
|
|
37
|
+
|
|
38
|
+
### Meta
|
|
39
|
+
|
|
40
|
+
- Engine swap 1.9.4 -> 1.9.7 regression battery (interleaved
|
|
41
|
+
best-of-3): cold walk 4,637 vs 4,652 µs, DOM parse 72 vs 73 µs,
|
|
42
|
+
SAX text-only 22.7 vs 22.6 ms — parity across the board. The
|
|
43
|
+
v1.9.6 XPath fixes (`/descendant::` root inclusion, $var head
|
|
44
|
+
dropping) are covered by the existing suite plus the new specs.
|
|
45
|
+
|
|
8
46
|
## [1.9.25] - 2026-08-28
|
|
9
47
|
|
|
10
48
|
### Fixed
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
9
|
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
10
|
# CHANGELOG when libleptris releases.
|
|
11
|
-
LIBLEPTRIS_VERSION = "1.9.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.7"
|
|
12
12
|
|
|
13
13
|
CMAKE_FLAGS = %w[
|
|
14
14
|
-DCMAKE_BUILD_TYPE=Release
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -151,6 +151,26 @@ class Leptris::XML::Document
|
|
|
151
151
|
Leptris::XML::Node.wrap(ptr, self)
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
# The document node — navigation head over the whole tree chain
|
|
155
|
+
# [prolog comments/PIs, root element, epilog comments/PIs] in
|
|
156
|
+
# document order (libleptris 1.9.7, upstream #580: the libxml2
|
|
157
|
+
# model; XPath /comment() and //processing-instruction() see the
|
|
158
|
+
# document-level nodes). A stable, document-owned singleton —
|
|
159
|
+
# Node.wrap's cache keeps the returned wrapper identical.
|
|
160
|
+
def node
|
|
161
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
162
|
+
@node ||= Leptris::XML::Node.wrap(
|
|
163
|
+
Leptris::XML::FFI.leptris_document_node(@c_ptr), self)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The document's children, via the document node: prolog
|
|
167
|
+
# comments/PIs, the root element, epilog comments/PIs, in
|
|
168
|
+
# document order (Nokogiri-parity shape).
|
|
169
|
+
def children
|
|
170
|
+
doc_node = node
|
|
171
|
+
doc_node ? doc_node.children : Leptris::XML::NodeSet.new(self, [])
|
|
172
|
+
end
|
|
173
|
+
|
|
154
174
|
# Attach +element+ as the document's root element. The element must
|
|
155
175
|
# have been created against this document and must not already have
|
|
156
176
|
# a parent. Any previous root is left detached (still owned by the
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -143,6 +143,11 @@ module Leptris
|
|
|
143
143
|
[:leptris_document, :leptris_element], :leptris_status
|
|
144
144
|
attach_function :leptris_document_root,
|
|
145
145
|
[:leptris_document], :leptris_element
|
|
146
|
+
# The document node — navigation head over the whole tree
|
|
147
|
+
# chain [prolog comments/PIs, root, epilog...] (libleptris
|
|
148
|
+
# 1.9.7, upstream #580; the libxml2 model).
|
|
149
|
+
attach_function :leptris_document_node,
|
|
150
|
+
[:leptris_document], :leptris_node_ref
|
|
146
151
|
# Document-level PIs (v1.6.0): not tree nodes — enumerate via
|
|
147
152
|
# these accessors only.
|
|
148
153
|
attach_function :leptris_document_pi_count,
|
|
@@ -430,6 +435,14 @@ module Leptris
|
|
|
430
435
|
[:leptris_document, :leptris_element, :string], :leptris_xpath_result
|
|
431
436
|
attach_function :leptris_xpath_eval_with_vars,
|
|
432
437
|
[:leptris_document, :string, :leptris_xpath_var_set], :leptris_xpath_result
|
|
438
|
+
# Union semantics (NodeSet#xpath): N contexts, ONE call — the
|
|
439
|
+
# engine merges the per-context results into a de-duplicated,
|
|
440
|
+
# document-ordered nodeset (libleptris 1.9.7, upstream #589).
|
|
441
|
+
attach_function :leptris_xpath_eval_nodeset,
|
|
442
|
+
[:leptris_document, :pointer, :size_t, :string], :leptris_xpath_result
|
|
443
|
+
attach_function :leptris_xpath_compiled_eval_nodeset,
|
|
444
|
+
[:leptris_document, :pointer, :size_t, :leptris_xpath_compiled],
|
|
445
|
+
:leptris_xpath_result
|
|
433
446
|
attach_function :leptris_xpath_eval_with_vars_context,
|
|
434
447
|
[:leptris_document, :leptris_element, :string, :leptris_xpath_var_set],
|
|
435
448
|
:leptris_xpath_result
|
|
@@ -552,12 +565,16 @@ module Leptris
|
|
|
552
565
|
[:string], :leptris_pull_parser
|
|
553
566
|
attach_function :leptris_pull_next,
|
|
554
567
|
[:leptris_pull_parser], :pointer
|
|
568
|
+
attach_function :leptris_pull_next_batch,
|
|
569
|
+
[:leptris_pull_parser, :pointer, :size_t], :size_t
|
|
555
570
|
attach_function :leptris_pull_attr_count,
|
|
556
571
|
[:leptris_pull_parser], :size_t
|
|
557
572
|
attach_function :leptris_pull_attr_name,
|
|
558
573
|
[:leptris_pull_parser, :size_t], :string
|
|
559
574
|
attach_function :leptris_pull_attr_value,
|
|
560
575
|
[:leptris_pull_parser, :size_t], :string
|
|
576
|
+
attach_function :leptris_pull_attrs,
|
|
577
|
+
[:leptris_pull_parser, :pointer, :size_t], :size_t
|
|
561
578
|
attach_function :leptris_pull_free,
|
|
562
579
|
[:leptris_pull_parser], :void
|
|
563
580
|
|
|
@@ -908,6 +925,20 @@ module Leptris
|
|
|
908
925
|
end
|
|
909
926
|
ptr
|
|
910
927
|
end
|
|
928
|
+
|
|
929
|
+
# Pull-event staging block for leptris_pull_next_batch (the
|
|
930
|
+
# #589 bulk transport): LeptrisPullEvent records, stride from
|
|
931
|
+
# the struct layout that mirrors the ABI.
|
|
932
|
+
def self.scratch_events(count)
|
|
933
|
+
need = count * PullEventStruct.size
|
|
934
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
935
|
+
ptr = scratch[:pull_events]
|
|
936
|
+
if ptr.nil? || ptr.size < need
|
|
937
|
+
ptr&.free
|
|
938
|
+
ptr = scratch[:pull_events] = ::FFI::MemoryPointer.new(need)
|
|
939
|
+
end
|
|
940
|
+
ptr
|
|
941
|
+
end
|
|
911
942
|
end
|
|
912
943
|
end
|
|
913
944
|
end
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -127,24 +127,23 @@ class Leptris::XML::NodeSet
|
|
|
127
127
|
end
|
|
128
128
|
alias_method :text, :inner_text
|
|
129
129
|
|
|
130
|
-
# Union semantics:
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
130
|
+
# Union semantics: ONE engine call evaluates the expression
|
|
131
|
+
# against every element member and merges the results into a
|
|
132
|
+
# de-duplicated, document-ordered nodeset (libleptris 1.9.7,
|
|
133
|
+
# upstream #589 — the per-member Ruby loop this replaces could
|
|
134
|
+
# neither dedup nor order).
|
|
134
135
|
def xpath(*paths)
|
|
135
136
|
handler, _ns, _vars = parse_search_args(paths)
|
|
136
137
|
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
137
138
|
expr = paths.join(" | ")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
147
|
-
Leptris::XML::NodeSet.new(@document, accumulated)
|
|
139
|
+
contexts = to_a.select { |node| node.is_a?(Leptris::XML::Element) }
|
|
140
|
+
return Leptris::XML::NodeSet.new(@document, []) if contexts.empty?
|
|
141
|
+
buffer = Leptris::XML::FFI.scratch_pointers(contexts.size)
|
|
142
|
+
buffer.write_array_of_pointer(contexts.map(&:c_ptr))
|
|
143
|
+
result_ptr = Leptris::XML::FFI.leptris_xpath_eval_nodeset(
|
|
144
|
+
@document.c_ptr, buffer, contexts.size, expr)
|
|
145
|
+
return Leptris::XML::NodeSet.new(@document, []) if result_ptr.null?
|
|
146
|
+
Leptris::XML::NodeSet.from_result(@document, result_ptr)
|
|
148
147
|
end
|
|
149
148
|
|
|
150
149
|
def inspect
|
data/lib/leptris/xml/pull.rb
CHANGED
|
@@ -14,7 +14,10 @@ require "ffi"
|
|
|
14
14
|
# end
|
|
15
15
|
#
|
|
16
16
|
module Leptris::XML::Pull
|
|
17
|
-
|
|
17
|
+
# Constructed positionally by the parser: the keyword-init
|
|
18
|
+
# variant cost ~220ns/event in batch drains (round XXI). Readers
|
|
19
|
+
# (event.type, event.attrs, ...) are unchanged.
|
|
20
|
+
Event = Struct.new(:type, :name, :text, :attrs)
|
|
18
21
|
|
|
19
22
|
TYPES = {
|
|
20
23
|
Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
|
|
@@ -60,6 +63,49 @@ module Leptris::XML::Pull
|
|
|
60
63
|
self
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
# Bulk-delivers events (libleptris 1.9.7, upstream #589): ONE C
|
|
67
|
+
# call stages up to +max_count+ events — the per-event dispatch
|
|
68
|
+
# that made streaming ~145x slower than a DOM parse collapses to
|
|
69
|
+
# amortized noise. Per the engine's protocol, attributes are
|
|
70
|
+
# captured only for the LAST start_element of each batch
|
|
71
|
+
# (earlier starts in the same batch carry nil attrs — segment
|
|
72
|
+
# boundaries fall where they fall); use #each when every
|
|
73
|
+
# start's attrs must be captured.
|
|
74
|
+
def each_batch(max_count = 256)
|
|
75
|
+
return enum_for(:each_batch, max_count) unless block_given?
|
|
76
|
+
stride = Leptris::XML::FFI::PullEventStruct.size
|
|
77
|
+
type_off = 0
|
|
78
|
+
name_off = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
|
|
79
|
+
text_off = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
|
|
80
|
+
buffer = Leptris::XML::FFI.scratch_events(max_count)
|
|
81
|
+
while (n = Leptris::XML::FFI.leptris_pull_next_batch(
|
|
82
|
+
@handle, buffer, max_count)) > 0
|
|
83
|
+
events = Array.new(n) do |i|
|
|
84
|
+
base = i * stride
|
|
85
|
+
name_ptr = buffer.get_pointer(base + name_off)
|
|
86
|
+
text_ptr = buffer.get_pointer(base + text_off)
|
|
87
|
+
Event.new(
|
|
88
|
+
TYPES[buffer.get_int(base + type_off)],
|
|
89
|
+
name_ptr.null? ? nil :
|
|
90
|
+
name_ptr.read_string.force_encoding(Encoding::UTF_8),
|
|
91
|
+
text_ptr.null? ? nil :
|
|
92
|
+
text_ptr.read_string.force_encoding(Encoding::UTF_8),
|
|
93
|
+
nil
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
# The attr mirror holds the batch's most recent start.
|
|
97
|
+
last_start = nil
|
|
98
|
+
events.each_with_index do |event, i|
|
|
99
|
+
last_start = i if event.type == :start_element
|
|
100
|
+
end
|
|
101
|
+
events[last_start].attrs = capture_attrs if last_start
|
|
102
|
+
events.each { |event| yield event }
|
|
103
|
+
tail = events.last.type
|
|
104
|
+
break if tail == :end_document || tail == :error
|
|
105
|
+
end
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
63
109
|
# Hot-loop offsets: derived from PullEventStruct's layout (the
|
|
64
110
|
# struct stays the single source of truth for the ABI), read via
|
|
65
111
|
# get_int/get_pointer so the per-event struct-wrapper allocation
|
|
@@ -80,23 +126,32 @@ module Leptris::XML::Pull
|
|
|
80
126
|
text_ptr = raw.get_pointer(TEXT_OFFSET)
|
|
81
127
|
attrs = type == :start_element ? capture_attrs : nil
|
|
82
128
|
Event.new(
|
|
83
|
-
type
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
attrs
|
|
129
|
+
type,
|
|
130
|
+
name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
|
|
131
|
+
text_ptr.null? ? nil : text_ptr.read_string.force_encoding(Encoding::UTF_8),
|
|
132
|
+
attrs
|
|
87
133
|
)
|
|
88
134
|
end
|
|
89
135
|
|
|
90
136
|
private
|
|
91
137
|
|
|
138
|
+
# One count-only query plus one flat copy (leptris_pull_attrs,
|
|
139
|
+
# 1.9.7) — the char** wire format the SAX callbacks already use —
|
|
140
|
+
# replaces the 2N per-index dispatches.
|
|
92
141
|
def capture_attrs
|
|
93
|
-
|
|
94
|
-
return nil if
|
|
142
|
+
total = Leptris::XML::FFI.leptris_pull_attrs(@handle, nil, 0)
|
|
143
|
+
return nil if total.zero?
|
|
144
|
+
buffer = Leptris::XML::FFI.scratch_pointers(2 * total)
|
|
145
|
+
copied = Leptris::XML::FFI.leptris_pull_attrs(@handle, buffer, total)
|
|
146
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
95
147
|
hash = {}
|
|
96
148
|
i = 0
|
|
97
|
-
while i <
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
while i < copied
|
|
150
|
+
name = buffer.get_pointer(2 * i * ptr_size).read_string
|
|
151
|
+
.force_encoding(Encoding::UTF_8)
|
|
152
|
+
value = buffer.get_pointer((2 * i + 1) * ptr_size).read_string
|
|
153
|
+
.force_encoding(Encoding::UTF_8)
|
|
154
|
+
hash[name] = value
|
|
100
155
|
i += 1
|
|
101
156
|
end
|
|
102
157
|
hash
|
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.26
|
|
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-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|