leptris 1.9.1 → 1.9.2
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 +23 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document_fragment.rb +5 -12
- data/lib/leptris/xml/ffi.rb +99 -0
- data/lib/leptris/xml/node.rb +5 -7
- data/lib/leptris/xml/node_set.rb +16 -22
- data/lib/leptris/xml/pull.rb +5 -10
- data/lib/leptris/xml/searchable.rb +3 -12
- data/lib/leptris/xml/serialization.rb +4 -18
- data/lib/leptris/xml/xpath.rb +1 -8
- 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: 6d6cc4a584231a903d60b34a8b3e438c729d804743f66ada5b9944dac2c2662e
|
|
4
|
+
data.tar.gz: 2cfcc126bf8e78363d6614e06ddfed0d72bd653eac0087f80781e237a7c1f85a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fe8c57e921c84a0c31552c255151e1f0a756236282b7d8ac184a7f54da5dcac465747d33acc77fbd38d47506fcfc7d89961711c7ae3daf45aa08cbc9ef337fe
|
|
7
|
+
data.tar.gz: ea5561b2e483f0dc17c9942d60763565873f3d983b2f188e71153afd60e83909ce53a40a61d3c73e15be13592e0c00f2752aaad9cca8365e3e68d5b2bd5455a8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.2] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
Architecture deepening — all C ABI and buffer knowledge now lives
|
|
13
|
+
behind the FFI seam (`lib/leptris/xml/ffi.rb`); no public API change.
|
|
14
|
+
|
|
15
|
+
- `PullEventStruct` (FFI::Struct) replaces the pull parser's
|
|
16
|
+
hand-rolled ABI offsets (`get_int(0)` / `get_pointer(8)` /
|
|
17
|
+
`get_pointer(16)`); struct-layout changes become a one-line
|
|
18
|
+
layout edit instead of silent offset drift.
|
|
19
|
+
- `FFI.with_ns_set(hash)` concentrates the namespace-binding
|
|
20
|
+
lifecycle (flatten → CStringArray wire format → build → yield →
|
|
21
|
+
free) that ad-hoc and compiled XPath each hand-wrote.
|
|
22
|
+
- `FFI.serialize_into_string` / `FFI.fetch_children` /
|
|
23
|
+
`FFI.fetch_result_nodes` / `FFI.parse_fragment_with_status` own
|
|
24
|
+
the size-query/allocate/fill/read buffer protocols; Serialization,
|
|
25
|
+
NodeSet and DocumentFragment now contain no `MemoryPointer` code.
|
|
26
|
+
- `Node#children` and `DocumentFragment#children` fetch all child
|
|
27
|
+
handles in one batched call (the libleptris 1.7.0 surface, now
|
|
28
|
+
attached) instead of the first_child + N next_sibling walk —
|
|
29
|
+
N+1 FFI round trips collapse to 2 dispatches plus N wraps.
|
|
30
|
+
|
|
8
31
|
## [1.9.1] - 2026-08-24
|
|
9
32
|
|
|
10
33
|
### Fixed
|
data/lib/leptris/version.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ffi"
|
|
4
|
-
|
|
5
3
|
# Wraps the synthetic "#document-fragment" element returned by
|
|
6
4
|
# leptris_parse_fragment. Children of this fragment are the parsed nodes;
|
|
7
5
|
# the fragment itself isn't part of any document tree but borrows its
|
|
@@ -15,23 +13,18 @@ class Leptris::XML::DocumentFragment
|
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
def self.parse(xml, document)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr)
|
|
16
|
+
raw, status = Leptris::XML::FFI.parse_fragment_with_status(
|
|
17
|
+
xml.to_s, document.c_ptr)
|
|
21
18
|
if raw.null?
|
|
22
19
|
raise Leptris::XML::ParseError,
|
|
23
|
-
"leptris_parse_fragment failed (status=#{
|
|
20
|
+
"leptris_parse_fragment failed (status=#{status})"
|
|
24
21
|
end
|
|
25
22
|
new(document, raw)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def children
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
32
|
-
until ptr.nil? || ptr.null?
|
|
33
|
-
nodes << Leptris::XML::Node.wrap(ptr, @document)
|
|
34
|
-
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
26
|
+
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
27
|
+
Leptris::XML::Node.wrap(ptr, @document)
|
|
35
28
|
end
|
|
36
29
|
Leptris::XML::NodeSet.new(@document, nodes)
|
|
37
30
|
end
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -80,6 +80,17 @@ module Leptris
|
|
|
80
80
|
:recover, :int
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# struct LeptrisPullEvent (leptris/sax.h): FFI::Struct derives
|
|
84
|
+
# the field offsets from the layout, so ABI changes are a
|
|
85
|
+
# one-line edit here instead of silent offset drift in callers.
|
|
86
|
+
class PullEventStruct < ::FFI::Struct
|
|
87
|
+
layout \
|
|
88
|
+
:type, :int,
|
|
89
|
+
:name, :pointer,
|
|
90
|
+
:text, :pointer,
|
|
91
|
+
:text_len, :size_t
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
attach_function :leptris_version, [], :string
|
|
84
95
|
attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
|
|
85
96
|
|
|
@@ -158,6 +169,10 @@ module Leptris
|
|
|
158
169
|
[:leptris_node_ref], :leptris_node_ref
|
|
159
170
|
attach_function :leptris_node_child_count,
|
|
160
171
|
[:leptris_node_ref], :size_t
|
|
172
|
+
# libleptris 1.7.0: all-kind child batch — out_nodes=NULL is a
|
|
173
|
+
# count-only query returning the TOTAL across every child kind.
|
|
174
|
+
attach_function :leptris_node_children,
|
|
175
|
+
[:leptris_node_ref, :pointer, :size_t], :size_t
|
|
161
176
|
attach_function :leptris_node_as_element,
|
|
162
177
|
[:leptris_node_ref], :leptris_element
|
|
163
178
|
attach_function :leptris_element_as_node,
|
|
@@ -623,6 +638,90 @@ module Leptris
|
|
|
623
638
|
detail = leptris_last_error.to_s
|
|
624
639
|
detail.empty? ? base : "#{base} (#{detail})"
|
|
625
640
|
end
|
|
641
|
+
|
|
642
|
+
# Namespace-binding lifecycle, written once: flatten the
|
|
643
|
+
# prefix/URI hash into the alternating CStringArray wire
|
|
644
|
+
# format, build the caller-owned set, yield it, free it under
|
|
645
|
+
# all outcomes. Every eval variant uses this; none of them
|
|
646
|
+
# knows how a set is born or dies.
|
|
647
|
+
def self.with_ns_set(hash)
|
|
648
|
+
flat = hash.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
|
|
649
|
+
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
650
|
+
set = leptris_xpath_ns_set_new_from_pairs(buffer, flat.length / 2)
|
|
651
|
+
if set.null?
|
|
652
|
+
raise Leptris::XML::Error,
|
|
653
|
+
"leptris_xpath_ns_set_new_from_pairs failed"
|
|
654
|
+
end
|
|
655
|
+
begin
|
|
656
|
+
yield set
|
|
657
|
+
ensure
|
|
658
|
+
leptris_xpath_ns_set_free(set)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Caller-buffer serialization cycle: size query (buf=NULL),
|
|
663
|
+
# allocate exactly, fill, read. Since libleptris 1.9.0 the
|
|
664
|
+
# pair reuses one serialization through the per-document
|
|
665
|
+
# cache. +ffi_function+ is leptris_document_serialize_into or
|
|
666
|
+
# leptris_element_serialize_into. The buffer holds the
|
|
667
|
+
# serialization + NUL and XML output is NUL-free, so the
|
|
668
|
+
# bounded read is exact.
|
|
669
|
+
def self.serialize_into_string(ffi_function, c_ptr, options)
|
|
670
|
+
need = ffi_function.call(c_ptr, nil, 0, nil, options)
|
|
671
|
+
return "" if need.zero?
|
|
672
|
+
buffer = ::FFI::MemoryPointer.new(:char, need)
|
|
673
|
+
begin
|
|
674
|
+
ffi_function.call(c_ptr, buffer, need, nil, options)
|
|
675
|
+
buffer.read_string
|
|
676
|
+
ensure
|
|
677
|
+
buffer.free
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# All-kind child handle batch (libleptris 1.7.0): count-only
|
|
682
|
+
# query sizes the buffer, one fetch copies every child kind,
|
|
683
|
+
# one bulk read materializes the pointers.
|
|
684
|
+
def self.fetch_children(c_ptr)
|
|
685
|
+
count = leptris_node_children(c_ptr, nil, 0)
|
|
686
|
+
return [] if count.zero?
|
|
687
|
+
with_pointer_buffer(count) do |buffer|
|
|
688
|
+
copied = leptris_node_children(c_ptr, buffer, count)
|
|
689
|
+
buffer.get_array_of_pointer(0, copied)
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
694
|
+
# copies all node kinds into a caller buffer and hands back
|
|
695
|
+
# the pointer array.
|
|
696
|
+
def self.fetch_result_nodes(result_ptr, count)
|
|
697
|
+
return [] if count.zero?
|
|
698
|
+
with_pointer_buffer(count) do |buffer|
|
|
699
|
+
copied = leptris_xpath_result_get_nodes_ex(
|
|
700
|
+
result_ptr, buffer, nil, count)
|
|
701
|
+
buffer.get_array_of_pointer(0, copied)
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Fragment parse with the status out-param read: returns
|
|
706
|
+
# [document-or-fragment pointer, status].
|
|
707
|
+
def self.parse_fragment_with_status(xml, document_ptr)
|
|
708
|
+
status = ::FFI::MemoryPointer.new(:int)
|
|
709
|
+
raw = leptris_parse_fragment(
|
|
710
|
+
xml, xml.bytesize, document_ptr, status)
|
|
711
|
+
[raw, status.read_int]
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# The one buffer-allocation point for handle-array fetches:
|
|
715
|
+
# everything MemoryPointer-related above this line is either
|
|
716
|
+
# an attached C call or another seam helper.
|
|
717
|
+
def self.with_pointer_buffer(count)
|
|
718
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, count)
|
|
719
|
+
begin
|
|
720
|
+
yield buffer
|
|
721
|
+
ensure
|
|
722
|
+
buffer.free
|
|
723
|
+
end
|
|
724
|
+
end
|
|
626
725
|
end
|
|
627
726
|
end
|
|
628
727
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -107,14 +107,12 @@ class Leptris::XML::Node
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def children
|
|
110
|
-
# Immutable in readonly mode: the
|
|
111
|
-
#
|
|
110
|
+
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
111
|
+
# construction is paid once.
|
|
112
112
|
return @children if readonly_cached?(:@children)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
117
|
-
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
113
|
+
parent = as_element_or_self
|
|
114
|
+
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
115
|
+
Leptris::XML::Node.wrap(ptr, @document, parent: parent)
|
|
118
116
|
end
|
|
119
117
|
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
120
118
|
@children = result if @document&.readonly?
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -63,30 +63,24 @@ class Leptris::XML::NodeSet
|
|
|
63
63
|
def each
|
|
64
64
|
return enum_for(:each) unless block_given?
|
|
65
65
|
if @result_ptr
|
|
66
|
-
# Batch-fetch all node pointers
|
|
67
|
-
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
68
|
-
# just elements) and wrap each. Wrappers are cached
|
|
69
|
-
# via Node.wrap, so re-iteration of the same
|
|
66
|
+
# Batch-fetch all node pointers through the FFI seam
|
|
67
|
+
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
68
|
+
# not just elements) and wrap each. Wrappers are cached
|
|
69
|
+
# per-Document via Node.wrap, so re-iteration of the same
|
|
70
|
+
# NodeSet hits it.
|
|
70
71
|
n = length
|
|
71
72
|
if n > 0
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
(copied...n).each do |i|
|
|
84
|
-
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
85
|
-
next if ptr.null?
|
|
86
|
-
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
87
|
-
end
|
|
88
|
-
ensure
|
|
89
|
-
buf.free
|
|
73
|
+
pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
74
|
+
pointers.each do |ptr|
|
|
75
|
+
next if ptr.null?
|
|
76
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
77
|
+
end
|
|
78
|
+
# The batch accessor under-copies mixed-kind results (upstream
|
|
79
|
+
# leptris#477); fall back to per-index fetch for the remainder.
|
|
80
|
+
(pointers.length...n).each do |i|
|
|
81
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
82
|
+
next if ptr.null?
|
|
83
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
90
84
|
end
|
|
91
85
|
end
|
|
92
86
|
else
|
data/lib/leptris/xml/pull.rb
CHANGED
|
@@ -67,22 +67,17 @@ module Leptris::XML::Pull
|
|
|
67
67
|
def next_event
|
|
68
68
|
raw = Leptris::XML::FFI.leptris_pull_next(@handle)
|
|
69
69
|
return nil if raw.null?
|
|
70
|
-
|
|
71
|
-
type = TYPES[
|
|
72
|
-
name =
|
|
73
|
-
|
|
74
|
-
text = text_ptr.null? ? nil : text_ptr.read_string
|
|
70
|
+
event = Leptris::XML::FFI::PullEventStruct.new(raw)
|
|
71
|
+
type = TYPES[event[:type]]
|
|
72
|
+
name = read_owned(event[:name])
|
|
73
|
+
text = read_owned(event[:text])
|
|
75
74
|
attrs = type == :start_element ? capture_attrs : nil
|
|
76
75
|
Event.new(type: type, name: name, text: text, attrs: attrs)
|
|
77
76
|
end
|
|
78
77
|
|
|
79
78
|
private
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
# char* text; size_t text_len; } — pointer-sized fields follow the
|
|
83
|
-
# 4-byte enum on 64-bit ABIs (name at 8, text at 16).
|
|
84
|
-
def read_string_at(raw, offset)
|
|
85
|
-
ptr = raw.get_pointer(offset)
|
|
80
|
+
def read_owned(ptr)
|
|
86
81
|
ptr.null? ? nil : ptr.read_string
|
|
87
82
|
end
|
|
88
83
|
|
|
@@ -55,20 +55,11 @@ module Leptris::XML::Searchable
|
|
|
55
55
|
|
|
56
56
|
protected
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# the same wire format CStringArray owns), evaluates via
|
|
61
|
-
# leptris_xpath_eval_ns, and frees the set regardless of outcome.
|
|
58
|
+
# Evaluates against a caller-owned namespace binding set; the
|
|
59
|
+
# build/teardown lifecycle lives at the FFI seam (FFI.with_ns_set).
|
|
62
60
|
def xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
63
|
-
|
|
64
|
-
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
65
|
-
set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
|
|
66
|
-
buffer, flat.length / 2)
|
|
67
|
-
raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
|
|
68
|
-
begin
|
|
61
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
69
62
|
Leptris::XML::FFI.leptris_xpath_eval_ns(doc_ptr, context_ptr, expr, set)
|
|
70
|
-
ensure
|
|
71
|
-
Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
|
|
72
63
|
end
|
|
73
64
|
end
|
|
74
65
|
|
|
@@ -18,12 +18,9 @@ module Leptris::XML::Serialization
|
|
|
18
18
|
end
|
|
19
19
|
private_constant :DEFAULT_OPTIONS
|
|
20
20
|
|
|
21
|
-
# +ffi_function+ is a bound _serialize_into function
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# pair reuses a single serialization through a per-document cache
|
|
25
|
-
# (leptris#541) — no C-side result-string allocation, and repeated
|
|
26
|
-
# serializations of an unmutated document skip the serialize pass.
|
|
21
|
+
# +ffi_function+ is a bound _serialize_into function. The buffer
|
|
22
|
+
# cycle lives at the FFI seam (FFI.serialize_into_string); this
|
|
23
|
+
# module owns only options selection and construction.
|
|
27
24
|
def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
|
|
28
25
|
opts =
|
|
29
26
|
if indent.to_i.zero? && !no_decl && encoding.nil?
|
|
@@ -33,18 +30,7 @@ module Leptris::XML::Serialization
|
|
|
33
30
|
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
34
31
|
opts
|
|
35
32
|
end
|
|
36
|
-
|
|
37
|
-
return "" if need.zero?
|
|
38
|
-
buf = ::FFI::MemoryPointer.new(:char, need)
|
|
39
|
-
begin
|
|
40
|
-
ffi_function.call(c_ptr, buf, need, nil, opts.pointer)
|
|
41
|
-
# The buffer holds exactly the serialization + NUL; XML output
|
|
42
|
-
# is NUL-free, so a bounded read_string is exact. (Avoids the
|
|
43
|
-
# out_len param — no portable size_t read exists on Pointer.)
|
|
44
|
-
buf.read_string
|
|
45
|
-
ensure
|
|
46
|
-
buf.free
|
|
47
|
-
end
|
|
33
|
+
Leptris::XML::FFI.serialize_into_string(ffi_function, c_ptr, opts.pointer)
|
|
48
34
|
end
|
|
49
35
|
|
|
50
36
|
# +ffi_function+ is a bound FFI function taking
|
data/lib/leptris/xml/xpath.rb
CHANGED
|
@@ -40,16 +40,9 @@ class Leptris::XML::XPath
|
|
|
40
40
|
document = context.document
|
|
41
41
|
result_ptr =
|
|
42
42
|
if ns && !ns.empty?
|
|
43
|
-
|
|
44
|
-
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
45
|
-
set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
|
|
46
|
-
buffer, flat.length / 2)
|
|
47
|
-
raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
|
|
48
|
-
begin
|
|
43
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
49
44
|
Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
|
|
50
45
|
@handle, document.c_ptr, context_ptr(context), set)
|
|
51
|
-
ensure
|
|
52
|
-
Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
|
|
53
46
|
end
|
|
54
47
|
else
|
|
55
48
|
Leptris::XML::FFI.leptris_xpath_compiled_eval(
|
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.2
|
|
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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|