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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3e961e97c5ce359508474f70efda320521debdd4eb5e851241a82a49ffe7512
4
- data.tar.gz: 8fd855b22026aacac080e734658e115826b45e59dcbfee6bb01a0e2b024a8cd1
3
+ metadata.gz: 4bb5caf8a4c09cfd0ad16d3dc414d205e957ef1994875963bf5c4202666925cb
4
+ data.tar.gz: 65ff8352d6b7efc9bc7178605af80d6d336e6692b41826dcc6927b1e56d96478
5
5
  SHA512:
6
- metadata.gz: b1e58c09914cc12d9541410e1e5e46ff1c7135646bed22a46c6f796e41444ebdb0d039d1eabc1a5dc6790c6bcf2c3e1e31ae8fa6d8a33e25005138c5c0889fef
7
- data.tar.gz: 42acbecab9c56654468540fa1cf7cda8dd666023a0d40b8f298ef2e18691d50e1e577393a0fb8bee36db27b743cdddf349db745e3b9c1bbb817d0a8681687a78
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.3"
4
+ VERSION = "1.9.4"
5
5
  end
@@ -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))
@@ -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 = Leptris::XML::NodeSet.new(@document)
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
- sub = Leptris::XML::NodeSet.send(:from_result, @document, result_ptr)
131
- accumulated = merge_node_sets(accumulated, sub)
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
- attr_accessor :document, :encoding
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 = build_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 = build_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. The struct (and its callbacks) are
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("UTF-8"))
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.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.3
4
+ version: 1.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.