leptris 1.9.9 → 1.9.11
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 +44 -0
- data/README.adoc +20 -1
- data/Rakefile +23 -2
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +8 -0
- data/lib/leptris/xml/node_set.rb +11 -3
- data/lib/leptris/xml/sax/parser.rb +13 -2
- 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: 4e218d64aa7c30174d4fe7a22e47cc78c35f07cbfe3d0f22430f3ba411651143
|
|
4
|
+
data.tar.gz: 030eed3a6b5a7387da757ae20406591490a1f55893425d63a443c77631903a6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45c707da19fc4114c5f35641b799e104130a8d7c93d8fd365fe72cafec0b894f80e06dba7a73d0109b0fde77868d5db3836035411a3d9951d2f5c3db3c7c8a0f
|
|
7
|
+
data.tar.gz: '06388d993c6331e3089ad5c9f87056d978096f8bbd56598a1e8abdd37db03575e7d79989f3ed9ef5ba836d58804daaa1039d1b22e2c30c6b1bc4fb59d88fbd85'
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ 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.11] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Readonly `Element#[]` serves from the memoized attributes
|
|
13
|
+
hash** (materializing on demand): repeated reads become hash
|
|
14
|
+
lookups — no FFI dispatch, no lifetime guard. Completes the
|
|
15
|
+
readonly contract on its hottest member. Writable documents keep
|
|
16
|
+
the direct path (values can change).
|
|
17
|
+
- **Materialized NodeSets stop re-batching**: `each`, `[]`, and
|
|
18
|
+
`length` consult the materialized array first; iterating or
|
|
19
|
+
indexing after `to_a` no longer re-pays the count + fetch + wrap
|
|
20
|
+
pass. Repeated iteration measures 2.4x faster.
|
|
21
|
+
- **NodeSet negative indexes are consistent**: `ns[-1]` answers the
|
|
22
|
+
last element for lazy and eager sets alike (Ruby-Array slice
|
|
23
|
+
semantics, Nokogiri parity); previously only eager sets
|
|
24
|
+
supported them.
|
|
25
|
+
- **SAX start_element attributes build pairs in one pass** — no
|
|
26
|
+
intermediate flat string array or each_slice enumerator per
|
|
27
|
+
event (~13% on the SAX parse loop).
|
|
28
|
+
|
|
29
|
+
### Measured and rejected
|
|
30
|
+
|
|
31
|
+
- A compiled-expression cache for ad-hoc xpath (the round's main
|
|
32
|
+
hypothesis): compiled vs ad-hoc eval measured within 2.5% on
|
|
33
|
+
500-member loops — the engine's expression parse is effectively
|
|
34
|
+
free. Killed before implementation.
|
|
35
|
+
|
|
36
|
+
## [1.9.10] - 2026-08-26
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **README: Migrating from Nokogiri, caught up to the 1.9.x
|
|
41
|
+
surface** — the notable-differences list now covers the lifetime
|
|
42
|
+
contract (UseAfterFreeError on freed-document reads/mutations),
|
|
43
|
+
readonly mode, recover parsing + last_error_position, searchable
|
|
44
|
+
fragments, receiver-relative css, and expanded-name attribute
|
|
45
|
+
access, each with the migrating reader's context.
|
|
46
|
+
- **Spec organization**: the eight-round perf_surface_spec grab-bag
|
|
47
|
+
splits into one file per concern (seam_and_reads,
|
|
48
|
+
sax_and_query_paths, lifetime_contract, fragment_and_position,
|
|
49
|
+
receiver_relative_css). Suite count unchanged at 274; each file
|
|
50
|
+
is one scannable context.
|
|
51
|
+
|
|
8
52
|
## [1.9.9] - 2026-08-26
|
|
9
53
|
|
|
10
54
|
### Changed
|
data/README.adoc
CHANGED
|
@@ -410,7 +410,26 @@ Notable differences:
|
|
|
410
410
|
* `Node#children` includes whitespace text nodes (same as Nokogiri); use
|
|
411
411
|
`#element_children` or `#first_element_child` to skip them.
|
|
412
412
|
* CSS support is intentionally minimal — for advanced selectors, drop to
|
|
413
|
-
`xpath`.
|
|
413
|
+
`xpath`. `css` is receiver-relative (Nokogiri semantics): scoped to
|
|
414
|
+
an element or fragment, document-wide from a Document.
|
|
415
|
+
* `DocumentFragment` is searchable (`fragment.xpath/at_xpath/css/at_css/search`)
|
|
416
|
+
— Nokogiri fragment parity.
|
|
417
|
+
* Expanded-name attribute access: `Element#attribute_ns(uri, local)` /
|
|
418
|
+
`#has_attribute_ns?(uri, local)` — XML Namespaces 1.0 semantics
|
|
419
|
+
(cross-prefix match, nil URI matches no-namespace, xmlns invisible).
|
|
420
|
+
* `Leptris::XML.parse(xml, recover: true)` returns an empty document
|
|
421
|
+
with the failure recorded on the thread-global last error instead of
|
|
422
|
+
raising ParseError — libxml2 `XML_PARSE_RECOVER` semantics. The
|
|
423
|
+
companion `Document#last_error_position` returns `[line, column]`.
|
|
424
|
+
* `Leptris::XML.parse(xml, readonly: true)` (or `Document#readonly!`)
|
|
425
|
+
freezes the document for reading: mutations raise ReadOnlyError, read
|
|
426
|
+
methods memoize aggressively. Faster steady state; no Nokogiri
|
|
427
|
+
equivalent.
|
|
428
|
+
* **Lifetime contract**: a borrowed handle used after the owning
|
|
429
|
+
document has been freed (or GC'd) raises `Leptris::XML::UseAfterFreeError`.
|
|
430
|
+
Nokogiri is silent on this — migrating code that holds Node references
|
|
431
|
+
past Document disposal will see the error; silence-replace-UAF patterns
|
|
432
|
+
from Nokogiri do not apply.
|
|
414
433
|
* No `Nokogiri::HTML` or `Nokogiri::CSS` parser. Leptris is XML-only.
|
|
415
434
|
* No XSLT, no RelaxNG / DTD validation, no schema caching.
|
|
416
435
|
* No built-in JRuby / TruffleRuby support — only CRuby via `ffi`.
|
data/Rakefile
CHANGED
|
@@ -55,11 +55,25 @@ task default: :spec
|
|
|
55
55
|
namespace :audit do
|
|
56
56
|
desc "Fail when ffi.rb attachments and library exports drift"
|
|
57
57
|
task :symbols do
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
# nm cannot read MSVC PE export tables (every symbol reports as
|
|
59
|
+
# unexported -> guaranteed false drift), so Windows skips: the
|
|
60
|
+
# darwin/linux CI legs enforce the mirror — any one platform's
|
|
61
|
+
# build of the same C surface suffices.
|
|
62
|
+
if Gem.win_platform?
|
|
63
|
+
puts "audit:symbols: skipped on Windows (PE export tables); " \
|
|
64
|
+
"darwin/linux legs enforce the mirror"
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
# Probe nm without a shell: the multi-arg system() execs
|
|
68
|
+
# directly, so there is no which/redirect syntax to be
|
|
69
|
+
# platform-hostile. File::NULL is NUL where it must be; a
|
|
70
|
+
# missing binary is falsy either way (ENOENT rescue belts the
|
|
71
|
+
# older spawn behavior).
|
|
72
|
+
unless nm_available?
|
|
60
73
|
puts "audit:symbols: skipped (nm unavailable on this platform)"
|
|
61
74
|
next
|
|
62
75
|
end
|
|
76
|
+
lib = Dir.glob("lib/libleptris.{dylib,so,dll}").first
|
|
63
77
|
unless lib
|
|
64
78
|
abort "audit:symbols: vendored library not found — run rake compile"
|
|
65
79
|
end
|
|
@@ -86,6 +100,13 @@ namespace :audit do
|
|
|
86
100
|
abort "audit:symbols: drift detected"
|
|
87
101
|
end
|
|
88
102
|
end
|
|
103
|
+
|
|
104
|
+
# @api private
|
|
105
|
+
def nm_available?
|
|
106
|
+
system("nm", "--version", out: File::NULL, err: File::NULL)
|
|
107
|
+
rescue Errno::ENOENT
|
|
108
|
+
false
|
|
109
|
+
end
|
|
89
110
|
end
|
|
90
111
|
|
|
91
112
|
require "rubygems/package_task"
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -32,6 +32,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def [](key)
|
|
35
|
+
# Readonly: the attributes hash cannot go stale (ADR 0003) and
|
|
36
|
+
# materializes on demand — repeated reads become hash lookups,
|
|
37
|
+
# with no dispatch and no lifetime guard (a hash read cannot
|
|
38
|
+
# use-after-free).
|
|
39
|
+
if @document&.readonly?
|
|
40
|
+
cached = attributes[key.to_s]
|
|
41
|
+
return cached.nil? ? nil : cached.value
|
|
42
|
+
end
|
|
35
43
|
ensure_alive!
|
|
36
44
|
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, key.to_s)
|
|
37
45
|
end
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -41,7 +41,8 @@ class Leptris::XML::NodeSet
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def length
|
|
44
|
-
@
|
|
44
|
+
return @array.length if @array
|
|
45
|
+
@result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : 0
|
|
45
46
|
end
|
|
46
47
|
alias_method :size, :length
|
|
47
48
|
|
|
@@ -49,19 +50,26 @@ class Leptris::XML::NodeSet
|
|
|
49
50
|
length == 0
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# The materialized array (after to_a) is authoritative: every
|
|
54
|
+
# reader consults it first so a materialized set stops paying the
|
|
55
|
+
# batch fetch on each iteration or index.
|
|
52
56
|
def [](idx)
|
|
57
|
+
return @array[idx] if @array
|
|
58
|
+
# Negative indexes are Ruby-Array semantics (Nokogiri's NodeSet
|
|
59
|
+
# is slice-like); materialize rather than answer nil
|
|
60
|
+
# inconsistently with an eager set.
|
|
61
|
+
return to_a[idx] if idx.negative?
|
|
53
62
|
if @result_ptr
|
|
54
63
|
return nil if idx < 0 || idx >= length
|
|
55
64
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
|
|
56
65
|
return nil if ptr.null?
|
|
57
66
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
58
|
-
else
|
|
59
|
-
@array[idx]
|
|
60
67
|
end
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def each
|
|
64
71
|
return enum_for(:each) unless block_given?
|
|
72
|
+
return @array.each { |n| yield n } if @array
|
|
65
73
|
if @result_ptr
|
|
66
74
|
# Batch-fetch all node pointers through the FFI seam
|
|
67
75
|
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
@@ -148,8 +148,19 @@ class Leptris::XML::SAX::Parser
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# The C `const char** attrs` is a NULL-terminated flat array of
|
|
151
|
-
# name/value pairs
|
|
151
|
+
# name/value pairs. Build the [name, value] pairs in one pass —
|
|
152
|
+
# no intermediate flat string array, no each_slice enumerator.
|
|
152
153
|
def walk_attr_array(attrs_ptr)
|
|
153
|
-
|
|
154
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
155
|
+
pairs = []
|
|
156
|
+
i = 0
|
|
157
|
+
loop do
|
|
158
|
+
name_ptr = attrs_ptr.get_pointer(i * ptr_size)
|
|
159
|
+
break if name_ptr.null?
|
|
160
|
+
value_ptr = attrs_ptr.get_pointer((i + 1) * ptr_size)
|
|
161
|
+
pairs << [utf8(name_ptr.read_string), utf8(value_ptr.read_string)]
|
|
162
|
+
i += 2
|
|
163
|
+
end
|
|
164
|
+
pairs
|
|
154
165
|
end
|
|
155
166
|
end
|