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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2e2451251a0c5aec3567f67ed11c48f1b48d1a9f3622bc8c3fe62f03192c185
4
- data.tar.gz: 6be47e0deac36287f736e5b6b7b9755ce4b1e2e4e3986959caa750c5072bcada
3
+ metadata.gz: 4e218d64aa7c30174d4fe7a22e47cc78c35f07cbfe3d0f22430f3ba411651143
4
+ data.tar.gz: 030eed3a6b5a7387da757ae20406591490a1f55893425d63a443c77631903a6f
5
5
  SHA512:
6
- metadata.gz: a352efddd7e68f48e17d7126bf1d003d8da0c2eb5cdf09be1c7c7d42b739a761208792e4b45033cd7dfd928123ff7db80c8d629a0f625554d778c7c6b822e4df
7
- data.tar.gz: 71c3504e8592fab25b026c68d25300c2420c22e932546257dd6d5cc819c47b9cf3ff82cbd80136d55e8250efe1c6b835a82d336b341d3cdbbcde8177ff16933d
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
- lib = Dir.glob("lib/libleptris.{dylib,so,dll}").first
59
- unless system("which nm > /dev/null 2>&1")
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.9"
4
+ VERSION = "1.9.11"
5
5
  end
@@ -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
@@ -41,7 +41,8 @@ class Leptris::XML::NodeSet
41
41
  end
42
42
 
43
43
  def length
44
- @result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : @array.length
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; CStringArray owns the format, we slice into 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
- Leptris::XML::CStringArray.to_ruby(attrs_ptr).each_slice(2).to_a
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
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.9
4
+ version: 1.9.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.