leptris 1.9.3 → 1.9.4
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/iterparse.rb +3 -0
- data/lib/leptris/xml/node_set.rb +8 -10
- data/lib/leptris/xml/sax/parser.rb +34 -15
- data/lib/leptris/xml/searchable.rb +4 -1
- 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: 4bb5caf8a4c09cfd0ad16d3dc414d205e957ef1994875963bf5c4202666925cb
|
|
4
|
+
data.tar.gz: 65ff8352d6b7efc9bc7178605af80d6d336e6692b41826dcc6927b1e56d96478
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 027f61b41cb7a0b9270ca974ca82b0d227b02598f21f757aeebadc3d1158d8a8d7335a2657512756e3143b446222817520074d83071065520209714889085951
|
|
7
|
+
data.tar.gz: 93c71428856e7d460fce88242ab315f63f18382806f76ecd4f7cedcb5f87a55cbe3890ed2f9937dd0dbd0be0a38dbbc16e8900803dcf116112221d3cf517ed36
|
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.4] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **SAX callback strings arrive UTF-8**: element names, comments,
|
|
13
|
+
CDATA, PI target/data, prefix mappings, and error messages now
|
|
14
|
+
cross the seam as UTF-8 (previously ASCII-8BIT — only `characters`
|
|
15
|
+
was corrected). Non-ASCII SAX content no longer leaks BINARY into
|
|
16
|
+
handler code.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **NodeSet#xpath de-churn**: search arguments parse once (not per
|
|
21
|
+
member), results accumulate into one plain array, and a single
|
|
22
|
+
NodeSet is constructed at the end — the per-member
|
|
23
|
+
`merge_node_sets` (two `to_a`s + a throwaway NodeSet per element)
|
|
24
|
+
is gone. ~16% on a 500-member union query. Batch-context eval
|
|
25
|
+
asked upstream (leptris/leptris#560).
|
|
26
|
+
- **SAX handler struct memoized** per handler; `Parser#document=`
|
|
27
|
+
invalidates it, so swapping the handler takes effect on the next
|
|
28
|
+
parse (previously the wiring was rebuilt per parse and the
|
|
29
|
+
struct ↔ handler coupling was only implied).
|
|
30
|
+
- **XPath variable bindings now raise ArgumentError** instead of
|
|
31
|
+
being silently ignored (no var-bound eval path exists; wire the
|
|
32
|
+
real thing when a use case arrives). A single trailing hash
|
|
33
|
+
remains the namespace hash (Nokogiri semantics).
|
|
34
|
+
- Failed XPath evaluations raise `XPathError` with the engine's
|
|
35
|
+
last-error detail from both the ad-hoc and compiled paths
|
|
36
|
+
(previously the ad-hoc path used the generic status string).
|
|
37
|
+
|
|
8
38
|
## [1.9.3] - 2026-08-25
|
|
9
39
|
|
|
10
40
|
### Fixed
|
data/lib/leptris/version.rb
CHANGED
|
@@ -18,6 +18,9 @@ require "ffi"
|
|
|
18
18
|
# Document; #document returns nil and they must not outlive the
|
|
19
19
|
# iteration.
|
|
20
20
|
class Leptris::XML::Iterparse
|
|
21
|
+
# NOTE: an IO argument is read fully into memory before the C
|
|
22
|
+
# iterator starts — the bounded-memory path is parse_file (C-side
|
|
23
|
+
# file streaming). The C API takes one (xml, len) buffer.
|
|
21
24
|
def self.parse(xml_or_io, &block)
|
|
22
25
|
xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
|
|
23
26
|
iterator = new(Leptris::XML::FFI.leptris_iterparse_new(xml, xml.bytesize))
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -117,29 +117,27 @@ class Leptris::XML::NodeSet
|
|
|
117
117
|
end
|
|
118
118
|
alias_method :text, :inner_text
|
|
119
119
|
|
|
120
|
+
# Union semantics: evaluate the expression against each element
|
|
121
|
+
# member (one C eval per member — the engine has no multi-context
|
|
122
|
+
# eval yet; leptris/leptris ask pending) and accumulate into ONE
|
|
123
|
+
# NodeSet, instead of merging NodeSets per member.
|
|
120
124
|
def xpath(*paths)
|
|
121
125
|
handler, _ns, _vars = parse_search_args(paths)
|
|
122
126
|
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
123
127
|
expr = paths.join(" | ")
|
|
124
|
-
accumulated =
|
|
128
|
+
accumulated = []
|
|
125
129
|
each do |node|
|
|
126
130
|
next unless node.is_a?(Leptris::XML::Element)
|
|
127
131
|
result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
|
|
128
132
|
@document.c_ptr, node.c_ptr, expr)
|
|
129
133
|
next if result_ptr.null?
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
accumulated.concat(
|
|
135
|
+
Leptris::XML::NodeSet.from_result(@document, result_ptr).to_a)
|
|
132
136
|
end
|
|
133
|
-
accumulated
|
|
137
|
+
Leptris::XML::NodeSet.new(@document, accumulated)
|
|
134
138
|
end
|
|
135
139
|
|
|
136
140
|
def inspect
|
|
137
141
|
to_a.inspect
|
|
138
142
|
end
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
def merge_node_sets(a, b)
|
|
143
|
-
Leptris::XML::NodeSet.new(@document, a.to_a + b.to_a)
|
|
144
|
-
end
|
|
145
143
|
end
|
|
@@ -6,13 +6,27 @@ class Leptris::XML::SAX::Parser
|
|
|
6
6
|
CHUNK_SIZE = 4096
|
|
7
7
|
private_constant :CHUNK_SIZE
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
attr_reader :document, :encoding
|
|
10
10
|
|
|
11
11
|
def initialize(handler = Leptris::XML::SAX::Document.new, encoding = nil)
|
|
12
12
|
@document = handler
|
|
13
13
|
@encoding = encoding
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Swapping the handler invalidates the memoized callback struct so
|
|
17
|
+
# the next parse dispatches to the new handler.
|
|
18
|
+
def document=(handler)
|
|
19
|
+
@document = handler
|
|
20
|
+
@handler_struct = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The handler struct (eleven FFI::Function callbacks + the C
|
|
24
|
+
# struct) is built once per handler and reused across parses; the
|
|
25
|
+
# memoized struct is anchored against GC by the instance itself.
|
|
26
|
+
def handler_struct
|
|
27
|
+
@handler_struct ||= build_handler_struct
|
|
28
|
+
end
|
|
29
|
+
|
|
16
30
|
# Parse a string, IO, or file path. Dispatches to parse_memory /
|
|
17
31
|
# parse_io / parse_file based on the argument type.
|
|
18
32
|
def parse(input)
|
|
@@ -26,7 +40,7 @@ class Leptris::XML::SAX::Parser
|
|
|
26
40
|
|
|
27
41
|
def parse_memory(string)
|
|
28
42
|
string = string.dup.force_encoding("UTF-8")
|
|
29
|
-
handler_struct =
|
|
43
|
+
handler_struct = self.handler_struct
|
|
30
44
|
rc = Leptris::XML::FFI.leptris_sax_parse(
|
|
31
45
|
string, string.bytesize, handler_struct.pointer, nil)
|
|
32
46
|
if rc != 0
|
|
@@ -37,7 +51,7 @@ class Leptris::XML::SAX::Parser
|
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
def parse_io(io)
|
|
40
|
-
handler_struct =
|
|
54
|
+
handler_struct = self.handler_struct
|
|
41
55
|
parser_ptr = Leptris::XML::FFI.leptris_sax_parser_create(
|
|
42
56
|
handler_struct.pointer, nil)
|
|
43
57
|
if parser_ptr.null?
|
|
@@ -69,9 +83,7 @@ class Leptris::XML::SAX::Parser
|
|
|
69
83
|
private
|
|
70
84
|
|
|
71
85
|
# Build a LeptrisSAXHandler struct populated with FFI::Function callbacks
|
|
72
|
-
# that dispatch to the Ruby handler.
|
|
73
|
-
# anchored against GC via the local variable for the duration of the
|
|
74
|
-
# synchronous parse call.
|
|
86
|
+
# that dispatch to the Ruby handler.
|
|
75
87
|
def build_handler_struct
|
|
76
88
|
s = Leptris::XML::FFI::SAXHandler.new
|
|
77
89
|
handler = @document # capture in closures
|
|
@@ -86,39 +98,39 @@ class Leptris::XML::SAX::Parser
|
|
|
86
98
|
|
|
87
99
|
s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
|
|
88
100
|
attrs = walk_attr_array(attrs_ptr)
|
|
89
|
-
handler.start_element(name, attrs)
|
|
101
|
+
handler.start_element(utf8(name), attrs)
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
|
|
93
|
-
handler.end_element(name)
|
|
105
|
+
handler.end_element(utf8(name))
|
|
94
106
|
end
|
|
95
107
|
|
|
96
108
|
s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
|
|
97
|
-
handler.characters(text_ptr.read_bytes(len).force_encoding(
|
|
109
|
+
handler.characters(text_ptr.read_bytes(len).force_encoding(Encoding::UTF_8))
|
|
98
110
|
end
|
|
99
111
|
|
|
100
112
|
s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
|
|
101
|
-
handler.comment(comment)
|
|
113
|
+
handler.comment(utf8(comment))
|
|
102
114
|
end
|
|
103
115
|
|
|
104
116
|
s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
|
|
105
|
-
handler.cdata_block(cdata)
|
|
117
|
+
handler.cdata_block(utf8(cdata))
|
|
106
118
|
end
|
|
107
119
|
|
|
108
120
|
s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
|
|
109
|
-
handler.processing_instruction(target, data)
|
|
121
|
+
handler.processing_instruction(utf8(target), utf8(data))
|
|
110
122
|
end
|
|
111
123
|
|
|
112
124
|
s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
|
|
113
|
-
handler.start_prefix_mapping(prefix, uri)
|
|
125
|
+
handler.start_prefix_mapping(utf8(prefix), utf8(uri))
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
|
|
117
|
-
handler.end_prefix_mapping(prefix)
|
|
129
|
+
handler.end_prefix_mapping(utf8(prefix))
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
|
|
121
|
-
handler.error(msg, line, col)
|
|
133
|
+
handler.error(utf8(msg), line, col)
|
|
122
134
|
end
|
|
123
135
|
|
|
124
136
|
s
|
|
@@ -128,6 +140,13 @@ class Leptris::XML::SAX::Parser
|
|
|
128
140
|
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
129
141
|
end
|
|
130
142
|
|
|
143
|
+
# FFI's implicit :string conversion does not set encoding — every
|
|
144
|
+
# callback string param crosses the seam as ASCII-8BIT unless
|
|
145
|
+
# corrected here (the headers contract UTF-8; nil for NULL).
|
|
146
|
+
def utf8(str)
|
|
147
|
+
str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
|
|
148
|
+
end
|
|
149
|
+
|
|
131
150
|
# The C `const char** attrs` is a NULL-terminated flat array of
|
|
132
151
|
# name/value pairs; CStringArray owns the format, we slice into pairs.
|
|
133
152
|
def walk_attr_array(attrs_ptr)
|
|
@@ -17,7 +17,7 @@ module Leptris::XML::Searchable
|
|
|
17
17
|
end
|
|
18
18
|
if result_ptr.null?
|
|
19
19
|
raise Leptris::XML::XPathError,
|
|
20
|
-
Leptris::XML::FFI.
|
|
20
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
|
|
@@ -72,6 +72,9 @@ module Leptris::XML::Searchable
|
|
|
72
72
|
break if args.empty?
|
|
73
73
|
end
|
|
74
74
|
ns, vars = hashes.reverse
|
|
75
|
+
unless vars.nil? || vars.empty?
|
|
76
|
+
raise ArgumentError, "XPath variable bindings are not supported"
|
|
77
|
+
end
|
|
75
78
|
[handler, ns, vars]
|
|
76
79
|
end
|
|
77
80
|
|