leptris 1.9.23 → 1.9.24
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 +28 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/sax/parser.rb +60 -24
- data/lib/leptris/xml/sax/recorder.rb +33 -10
- 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: e6d5a244d8c1bf6743629a19b9256da3df1159cd6fba153a61f0bdad6037b072
|
|
4
|
+
data.tar.gz: 37430812bf170d6e47f8a187cda3669e5b0e422e7420f1da2ec642cff13e1d76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cf2c7ccd09de5e89588db0bbad0fe9650f892608f6a4a4a08e4c40f5175768091a6f88ff1bedfd8d5b571eb04c9249c09daec7ed3ba99dff0ba1b89d426e0b0
|
|
7
|
+
data.tar.gz: '038b8c917cde736d84df62a3bbe9e7c870ccdb210184c0ac49c785d900805f5a5ed71f33e922d56c4abf9a3fa99bf38d0368976b49efa7da2f58d7fadc0dc648'
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,34 @@ 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.
|
|
8
|
+
## [1.9.24] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Interest-proportional SAX (both transports)**: the handler's
|
|
13
|
+
declared interest — which event methods it overrides — now decides
|
|
14
|
+
what the transport dispatches or drains.
|
|
15
|
+
- `SAX::Parser` attaches a callback only when the handler defines
|
|
16
|
+
the method beyond `SAX::Document`'s no-ops; the C engine already
|
|
17
|
+
skips NULL callbacks, so unwanted events cost nothing on either
|
|
18
|
+
side. Measured per parse of a 1.9 MB / 250k-event document:
|
|
19
|
+
text-only handler 151 ms -> 22 ms (**6.9x**); a handler that
|
|
20
|
+
overrides nothing runs at the C floor, 151 ms -> 7 ms
|
|
21
|
+
(**22x**); elements-only 151 ms -> 121 ms (-20%, the remainder
|
|
22
|
+
is attribute walking). Handlers overriding most kinds stay at
|
|
23
|
+
parity. Duck-typed handlers attach exactly what they define.
|
|
24
|
+
- `SAX::Recorder#each_event(*kinds)` (and `Recorder.parse(...,
|
|
25
|
+
kinds:)`, `#feed_stream(..., kinds:)`) skips records of other
|
|
26
|
+
kinds BEFORE slicing any strings from the arena — an unwanted
|
|
27
|
+
event costs one Array read. Filtered `:characters` drain 201 ms
|
|
28
|
+
-> 119 ms (**-41%**); unknown kinds raise ArgumentError. The
|
|
29
|
+
kind lookup table is now an Array indexed by the event code.
|
|
30
|
+
- Transport choice, by measurement: the pruned callback transport
|
|
31
|
+
beats the filtered recorder for selective consumers (C-side
|
|
32
|
+
emission is skipped entirely), so callbacks remain the default;
|
|
33
|
+
the recorder keeps its niche — bulk raw-event drains per chunk.
|
|
34
|
+
|
|
35
|
+
## [1.9.23] - 2026-08-27
|
|
9
36
|
|
|
10
37
|
### Added
|
|
11
38
|
|
data/lib/leptris/version.rb
CHANGED
|
@@ -83,59 +83,95 @@ class Leptris::XML::SAX::Parser
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
85
|
# Build a LeptrisSAXHandler struct populated with FFI::Function callbacks
|
|
86
|
-
# that dispatch to the Ruby handler.
|
|
86
|
+
# that dispatch to the Ruby handler. Only the callbacks the handler
|
|
87
|
+
# actually overrides are attached: the C engine skips NULL callbacks,
|
|
88
|
+
# so cost scales with the handler's declared interest, not with the
|
|
89
|
+
# document's event mix (measured 4.9x for an elements-only handler on
|
|
90
|
+
# a 1.9 MB stream).
|
|
87
91
|
def build_handler_struct
|
|
88
92
|
s = Leptris::XML::FFI::SAXHandler.new
|
|
89
93
|
handler = @document # capture in closures
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if overridden?(:start_document)
|
|
96
|
+
s[:start_document] = callback(:void, [:pointer]) do |_|
|
|
97
|
+
handler.start_document
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if overridden?(:end_document)
|
|
102
|
+
s[:end_document] = callback(:void, [:pointer]) do |_|
|
|
103
|
+
handler.end_document
|
|
104
|
+
end
|
|
97
105
|
end
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
if overridden?(:start_element)
|
|
108
|
+
s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
|
|
109
|
+
attrs = walk_attr_array(attrs_ptr)
|
|
110
|
+
handler.start_element(utf8(name), attrs)
|
|
111
|
+
end
|
|
102
112
|
end
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
if overridden?(:end_element)
|
|
115
|
+
s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
|
|
116
|
+
handler.end_element(utf8(name))
|
|
117
|
+
end
|
|
106
118
|
end
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
if overridden?(:characters)
|
|
121
|
+
s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
|
|
122
|
+
handler.characters(text_ptr.read_bytes(len).force_encoding(Encoding::UTF_8))
|
|
123
|
+
end
|
|
110
124
|
end
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
if overridden?(:comment)
|
|
127
|
+
s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
|
|
128
|
+
handler.comment(utf8(comment))
|
|
129
|
+
end
|
|
114
130
|
end
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
if overridden?(:cdata_block)
|
|
133
|
+
s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
|
|
134
|
+
handler.cdata_block(utf8(cdata))
|
|
135
|
+
end
|
|
118
136
|
end
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
if overridden?(:processing_instruction)
|
|
139
|
+
s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
|
|
140
|
+
handler.processing_instruction(utf8(target), utf8(data))
|
|
141
|
+
end
|
|
122
142
|
end
|
|
123
143
|
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
if overridden?(:start_prefix_mapping)
|
|
145
|
+
s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
|
|
146
|
+
handler.start_prefix_mapping(utf8(prefix), utf8(uri))
|
|
147
|
+
end
|
|
126
148
|
end
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
|
|
150
|
+
if overridden?(:end_prefix_mapping)
|
|
151
|
+
s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
|
|
152
|
+
handler.end_prefix_mapping(utf8(prefix))
|
|
153
|
+
end
|
|
130
154
|
end
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
if overridden?(:error)
|
|
157
|
+
s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
|
|
158
|
+
handler.error(utf8(msg), line, col)
|
|
159
|
+
end
|
|
134
160
|
end
|
|
135
161
|
|
|
136
162
|
s
|
|
137
163
|
end
|
|
138
164
|
|
|
165
|
+
# True when the handler defines the event method beyond the no-op
|
|
166
|
+
# every SAX::Document carries — subclasses, included modules, and
|
|
167
|
+
# duck-typed handlers all qualify; a handler that overrides nothing
|
|
168
|
+
# attaches nothing and the parse runs at the C floor.
|
|
169
|
+
def overridden?(name)
|
|
170
|
+
@document.method(name).owner != Leptris::XML::SAX::Document
|
|
171
|
+
rescue NameError
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
139
175
|
def callback(return_type, params, blocking: true, &block)
|
|
140
176
|
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
141
177
|
end
|
|
@@ -29,6 +29,10 @@ class Leptris::XML::SAX::Recorder
|
|
|
29
29
|
Leptris::XML::FFI::SAX_EVENT_ERROR => :error,
|
|
30
30
|
}.freeze
|
|
31
31
|
|
|
32
|
+
# Kind symbol per code byte — Array indexing in the drain loop,
|
|
33
|
+
# not a Hash lookup per event.
|
|
34
|
+
KIND_BY_CODE = Array.new(KINDS.size) { |code| KINDS[code] }.freeze
|
|
35
|
+
|
|
32
36
|
def self.open
|
|
33
37
|
raw = Leptris::XML::FFI.leptris_sax_recorder_new
|
|
34
38
|
if raw.null?
|
|
@@ -38,14 +42,17 @@ class Leptris::XML::SAX::Recorder
|
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
# One-shot parse over a complete document, yielding Record structs.
|
|
41
|
-
|
|
45
|
+
# +kinds+ filters the drain (see #each_event) — pass e.g.
|
|
46
|
+
# `kinds: [:start_element]` to slice strings only for the events
|
|
47
|
+
# you consume.
|
|
48
|
+
def self.parse(xml_or_io, kinds: nil)
|
|
42
49
|
recorder = open
|
|
43
50
|
begin
|
|
44
51
|
if xml_or_io.respond_to?(:read)
|
|
45
|
-
recorder.feed_stream(xml_or_io) { |*args| yield(*args) }
|
|
52
|
+
recorder.feed_stream(xml_or_io, kinds: kinds) { |*args| yield(*args) }
|
|
46
53
|
else
|
|
47
54
|
recorder.feed(xml_or_io.to_s, final: true)
|
|
48
|
-
recorder.each_event { |*args| yield(*args) }
|
|
55
|
+
recorder.each_event(*Array(kinds)) { |*args| yield(*args) }
|
|
49
56
|
end
|
|
50
57
|
ensure
|
|
51
58
|
recorder.free
|
|
@@ -72,19 +79,23 @@ class Leptris::XML::SAX::Recorder
|
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
# Feed an IO in chunks, draining events after each chunk.
|
|
75
|
-
def feed_stream(io, chunk_size = 65_536, &block)
|
|
82
|
+
def feed_stream(io, chunk_size = 65_536, kinds: nil, &block)
|
|
76
83
|
while (chunk = io.read(chunk_size))
|
|
77
84
|
feed(chunk)
|
|
78
|
-
each_event(&block)
|
|
85
|
+
each_event(*Array(kinds), &block)
|
|
79
86
|
end
|
|
80
87
|
feed("", final: true)
|
|
81
|
-
each_event(&block)
|
|
88
|
+
each_event(*Array(kinds), &block)
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
# Drains the current chunk's records: bulk-reads the record array
|
|
85
92
|
# and the packed arena (two FFI calls), then slices every string
|
|
86
|
-
# from the arena in Ruby — no per-string FFI.
|
|
87
|
-
|
|
93
|
+
# from the arena in Ruby — no per-string FFI. With +kinds+, records
|
|
94
|
+
# of other kinds are skipped BEFORE any string is sliced: an
|
|
95
|
+
# unwanted event costs one Array read, and cost scales with what
|
|
96
|
+
# the consumer asked for, not with the document's event mix.
|
|
97
|
+
def each_event(*kinds)
|
|
98
|
+
wanted = kinds.empty? ? nil : wanted_set(kinds)
|
|
88
99
|
count_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
89
100
|
len_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
90
101
|
begin
|
|
@@ -108,8 +119,8 @@ class Leptris::XML::SAX::Recorder
|
|
|
108
119
|
|
|
109
120
|
i = 0
|
|
110
121
|
while i < count
|
|
111
|
-
kind =
|
|
112
|
-
if kind
|
|
122
|
+
kind = KIND_BY_CODE[fields[i * 9]]
|
|
123
|
+
if kind && (wanted.nil? || wanted[kind])
|
|
113
124
|
base = i * 9 + 1
|
|
114
125
|
yield kind,
|
|
115
126
|
slice(arena, fields[base], fields[base + 1]),
|
|
@@ -128,6 +139,18 @@ class Leptris::XML::SAX::Recorder
|
|
|
128
139
|
|
|
129
140
|
private
|
|
130
141
|
|
|
142
|
+
def wanted_set(kinds)
|
|
143
|
+
unknown = kinds.each_with_object([]) do |kind, acc|
|
|
144
|
+
acc << kind unless KINDS.value?(kind)
|
|
145
|
+
end
|
|
146
|
+
unless unknown.empty?
|
|
147
|
+
raise ArgumentError,
|
|
148
|
+
"unknown event kinds #{unknown.inspect} " \
|
|
149
|
+
"(known: #{KINDS.values.inspect})"
|
|
150
|
+
end
|
|
151
|
+
kinds.each_with_object({}) { |kind, set| set[kind] = true }
|
|
152
|
+
end
|
|
153
|
+
|
|
131
154
|
# Zero-length slices are empty strings, not nil — the default
|
|
132
155
|
# namespace prefix is legitimately "" (START_PREFIX: name may be
|
|
133
156
|
# ""), matching the callback transport's delivery.
|