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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afa52b007ec23d222a3562207fcef3f9975f62c11378da91de2d238eff7a74be
4
- data.tar.gz: 878468ec897ba3f9b60aca28e1379ac712e261c4500c4cc51c2a7379545730ee
3
+ metadata.gz: 4c1650cd7d5f1be133b6c265c5ea6bfd646265ac443a86361bb30593dd0d255a
4
+ data.tar.gz: babd8da0971c8f6820f2692e7283d249cea85455f84620a73e86fe488caa1a18
5
5
  SHA512:
6
- metadata.gz: a8caa0b2d6052ff1e72a1d23c59d1f283b887c852e7cb669fa8449d85438f8f89a2005726f96644f6bc46c880d717e54c922210fb9b638c2d890d6d0cdc57fbe
7
- data.tar.gz: 19fab1e638971c2402b69e70447657c9dfe4d140ba55e588f09b4a394690b34c2e43ba46433615209f66be1660ee6755b83827e7b51e84516c5793b67f44d0d2
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.4"
11
+ LIBLEPTRIS_VERSION = "1.9.7"
12
12
 
13
13
  CMAKE_FLAGS = %w[
14
14
  -DCMAKE_BUILD_TYPE=Release
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.25"
4
+ VERSION = "1.9.26"
5
5
  end
@@ -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
@@ -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
@@ -127,24 +127,23 @@ class Leptris::XML::NodeSet
127
127
  end
128
128
  alias_method :text, :inner_text
129
129
 
130
- # Union semantics: evaluate the expression against each element
131
- # member (one C eval per member the engine has no multi-context
132
- # eval yet; leptris/leptris ask pending) and accumulate into ONE
133
- # NodeSet, instead of merging NodeSets per member.
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
- accumulated = []
139
- each do |node|
140
- next unless node.is_a?(Leptris::XML::Element)
141
- result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
142
- @document.c_ptr, node.c_ptr, expr)
143
- next if result_ptr.null?
144
- accumulated.concat(
145
- Leptris::XML::NodeSet.from_result(@document, result_ptr).to_a)
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
@@ -14,7 +14,10 @@ require "ffi"
14
14
  # end
15
15
  #
16
16
  module Leptris::XML::Pull
17
- Event = Struct.new(:type, :name, :text, :attrs, keyword_init: true)
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: type,
84
- name: name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
85
- text: text_ptr.null? ? nil : text_ptr.read_string.force_encoding(Encoding::UTF_8),
86
- attrs: 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
- count = Leptris::XML::FFI.leptris_pull_attr_count(@handle)
94
- return nil if count.zero?
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 < count
98
- hash[Leptris::XML::FFI.leptris_pull_attr_name(@handle, i)] =
99
- Leptris::XML::FFI.leptris_pull_attr_value(@handle, i)
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.25
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-27 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi