leptris 1.9.144.1 → 1.9.156.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '01429a81e019ba9e4ecf9d2a7c23ada3b4d2c6dc3849c5a0be469bbab52b5c92'
4
- data.tar.gz: 21bc4d37174f592fe72821f9fd7074eb7521c7ff4eacad42141720f2e235c97b
3
+ metadata.gz: c649879fc6a03c38e8c36df987c079271d62583992ae6836f23f53dae102a82e
4
+ data.tar.gz: 91c1a0d1f5101f73841bfee5ca6fd4ad07bbe36a9b6035c7542d3c43bac14b7e
5
5
  SHA512:
6
- metadata.gz: 181136f9e8da6a067bba876bbbfe5e841c062470fce5c6a521de413f5abd4a33d824eb387155021b486ff5063e70d96077156b79656440ee418d4b388bc5297c
7
- data.tar.gz: 3e088d8220ec7c3c40f4e9869caf8cc73e08606ac4f7035a9f8a277454fafc1d7921c11fe17aa64f4c328254362103962bf05218385e1855c27fad3663b5b915
6
+ metadata.gz: 12b2aaa87ab88a85f9c2023a976869a30ae53bb6671d8bf47d0097e87870a5b8eb3fe0b728d564ae6312b46f2343b2a3db5720cdd6b4ce27a86c1ddb9826547e
7
+ data.tar.gz: 5435202bd16c7175dfb1a36d49bf3027ed562729f7f99a1a98b393aa9daf85f0a338e019f953864ad07ee13be3457f15b2e7b6e730e67db9d8adf2c4b2f76703
data/CHANGELOG.md CHANGED
@@ -5,6 +5,121 @@ 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.156.2] - 2026-09-13
9
+
10
+ ### Added
11
+
12
+ - **#183: XPath version selection** (mirroring leptris-py#105):
13
+ `xpath`/`at_xpath` take a trailing `version:` selector —
14
+ `doc.xpath("count(//b)", version: "1.0")` evaluates through the
15
+ strict 1.0 engine lane (3.x syntax raises), `"3.1"` the full
16
+ grammar; `XPath.compile(expr, version:)` pins compiled
17
+ expressions the same way. Carried positionally — a declared
18
+ keyword is impossible without breaking the namespace-binding
19
+ hash channel (Ruby converts a trailing String-keyed hash into
20
+ keywords the moment one exists), so a `:version` key whose
21
+ VALUE is a version selector ("1.0"/"3.1"/:xpath10/:xpath31)
22
+ selects the channel; dotted-number values raise at the boundary
23
+ with the valid values; any other value stays a prefix binding
24
+ (a prefix literally named "version" keeps working). `version:`
25
+ combined with namespace bindings raises (the versioned engine
26
+ entry takes no ns set yet), as does `version:` on css. The nil
27
+ default keeps the existing eval path verbatim.
28
+ \n## [1.9.156.1] - 2026-09-13
29
+
30
+ ### Added
31
+
32
+ - **`Element#create_child(name)`** — the fused create+append
33
+ (libleptris 1.9.153 `leptris_element_create_child`, audit 290)
34
+ landed on main seconds after the 1.9.156.0 platform gems were
35
+ built and published, so that release ships without the binding
36
+ face. This patch release carries it: one C call, tree semantics
37
+ identical to `create_element` + `add_child`, the builder-shape
38
+ single call.
39
+
40
+ ## [1.9.156.0] - 2026-09-13
41
+
42
+ ### Security/correctness
43
+
44
+ - **Lockstep with libleptris 1.9.156 (1.9.152 → 1.9.156)** — carries
45
+ the **#1038 heap-corruption fix** (engine regression window:
46
+ libleptris 1.9.151–1.9.155; root-doc map entries outlived their
47
+ document and wrote through freed pools — roaming crashes in
48
+ downstream binding suites at ~5% of runs). The two gem versions
49
+ that embedded the affected engine, **1.9.152.0 and 1.9.152.1,
50
+ are YANKED**; upgrade to 1.9.156.0.
51
+
52
+ ### Added
53
+
54
+ - **`Element#create_child(name)`** (libleptris 1.9.153): the fused
55
+ create+append — one document resolution, one C call, tree
56
+ semantics identical to `create_element` + `add_child`; the
57
+ builder-shape single call.
58
+
59
+ ### Changed
60
+
61
+ - Engine: WHATWG template-mode gaps closed (1.9.154/1.9.155):
62
+ frame/frameset drops inside templates, in-column-group token
63
+ gating, `</template>` inside `<select>`, foreign-content
64
+ template end-tag matching — html5lib corpus 1197 → 1206;
65
+ Nokogiri parity floor held. Audit 289 → 290
66
+ (leptris_element_create_child).
67
+
68
+ ## [1.9.152.1] - 2026-09-13
69
+
70
+ ### Fixed
71
+
72
+ - **#178 (ask 1): attach seams lift in-scope namespace
73
+ declarations on adoption.** The engine's attach entries move a
74
+ subtree but not the source's in-scope declarations — a
75
+ cross-document (or out-of-scope) `add_child` /
76
+ `prepend_child` / `add_next_sibling` / `add_previous_sibling` /
77
+ `Document#root=` produced serialized output with UNDECLARED
78
+ prefixes. `Element.lift_namespaces_for_adoption` snapshots the
79
+ node's scope before the move and re-declares on the moved
80
+ element everything the target does not already resolve
81
+ identically (own declarations and identical target scopes add
82
+ nothing — same-scope moves stay output-identical).
83
+
84
+ ### Added
85
+
86
+ - **#178 (ask 3): `benchmark/consumer_pipeline.rb`** — the
87
+ model-pipeline leg: a nested 7000-item document walked into
88
+ typed objects, with `GC.stat` allocation deltas (load
89
+ independent: ~799k allocations per run) and fresh-process
90
+ one-shot medians (n=9, the CLI-workload shape where YJIT never
91
+ amortizes).
92
+
93
+ ## [1.9.156.0] - 2026-09-13
94
+
95
+ ### Changed
96
+
97
+ - **Lockstep with libleptris 1.9.156** (1.9.152 → 1.9.156; audit
98
+ 290/290 — new symbol `leptris_element_create_child` attached):
99
+ pulls in the leptris/leptris#1038 heap-corruption fix — stale
100
+ root-doc map entries (pool-fallback creates + XInclude
101
+ adopted-child free) resolved freed documents after address
102
+ recycling, corrupting the heap in ~5-10% of long-running
103
+ processes; entries now die with their document. 1.9.152 and
104
+ 1.9.152.1 shipped inside the affected range (1.9.151-154).
105
+ Also rides: WHATWG `<template>` cgroup drops and `</template>`
106
+ pop-through (html5lib corpus 1200 → 1206), frame/frameset
107
+ in-template drops (1.9.154), and the lane-18 DOM perf rows.
108
+
109
+ ## [1.9.152.0] - 2026-09-13
110
+
111
+ ### Changed
112
+
113
+ - **Lockstep with libleptris 1.9.152** (1.9.144 → 1.9.152; no new
114
+ C surface — audit 289/289): the lane-18 DOM mutation performance
115
+ arc rides the binding directly — attr-heavy parse up to 2.7x
116
+ faster (1.9.147/1.9.149), the DOM create path 91.6 → 6.6 ns/op
117
+ and the append row 401 → ~258 µs (1.9.150–1.9.152), set-attr row
118
+ 390 → 262 µs. WHATWG HTML: `<template>` insertion modes
119
+ (1.9.146/1.9.148) — an open template is a scope boundary for
120
+ table-context starts, and each open template tracks its saved
121
+ insertion mode (implied tr/tbody/rows per WHATWG 13.2.6.4.10).
122
+
8
123
  ## [1.9.144.1] - 2026-09-12
9
124
 
10
125
  ### 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.144"
11
+ LIBLEPTRIS_VERSION = "1.9.156"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.144.1"
4
+ VERSION = "1.9.156.2"
5
5
  end
@@ -244,6 +244,9 @@ class Leptris::XML::Document
244
244
  # document's pool until #free).
245
245
  def root=(element)
246
246
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
247
+ # A document root has no in-scope declarations of its own —
248
+ # lift everything the element's source scope carried (#178).
249
+ Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
247
250
  Leptris::XML::FFI.check_status(
248
251
  Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
249
252
  @version += 1
@@ -215,8 +215,35 @@ class Leptris::XML::Element < Leptris::XML::Node
215
215
  p ? "#{p}:#{name}" : name
216
216
  end
217
217
 
218
+ # Cross-document and cross-scope adoption (#178 ask 1): the
219
+ # engine's attach entries move a subtree but do not lift the
220
+ # source's in-scope namespace declarations, so serialized output
221
+ # can carry undeclared prefixes. Re-declares on +node+ everything
222
+ # from its in-scope set that +target_scope+ (the attach target's
223
+ # in-scope map) does not already resolve identically — the
224
+ # declarations ride the moved element, staying local to the
225
+ # adopted subtree. Call BEFORE attaching: the source scope is
226
+ # read through the node's current ancestors. Identical scopes
227
+ # (the common same-document move) add nothing.
228
+ def self.lift_namespaces_for_adoption(node, target_scope)
229
+ return unless node.is_a?(Leptris::XML::Element)
230
+ # Declarations the node already carries (its own definitions —
231
+ # preserved by the C copy and by moves) ride it as-is; lifting
232
+ # must not duplicate them, only what came from ANCESTORS.
233
+ own = node.namespace_definitions.each_with_object({}) do |ns, h|
234
+ h[ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"] = ns.href
235
+ end
236
+ node.namespaces.each do |key, uri|
237
+ next if target_scope[key] == uri || own[key] == uri
238
+ prefix = key.start_with?("xmlns:") ? key.delete_prefix("xmlns:") : nil
239
+ node.add_namespace_definition(prefix, uri)
240
+ end
241
+ nil
242
+ end
243
+
218
244
  def prepend_child(node)
219
245
  ensure_writable!
246
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
220
247
  Leptris::XML::FFI.check_status(
221
248
  Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
222
249
  node
@@ -224,6 +251,7 @@ class Leptris::XML::Element < Leptris::XML::Node
224
251
 
225
252
  def add_next_sibling(node)
226
253
  ensure_writable!
254
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
227
255
  Leptris::XML::FFI.check_status(
228
256
  Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
229
257
  node
@@ -231,6 +259,7 @@ class Leptris::XML::Element < Leptris::XML::Node
231
259
 
232
260
  def add_previous_sibling(node)
233
261
  ensure_writable!
262
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
234
263
  Leptris::XML::FFI.check_status(
235
264
  Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
236
265
  node
@@ -302,10 +331,24 @@ class Leptris::XML::Element < Leptris::XML::Node
302
331
  end
303
332
  alias_method :clone, :dup
304
333
 
334
+ # Fused create+append (libleptris 1.9.153): builds a child
335
+ # element of +name+ in this element's document and attaches it
336
+ # in ONE C call — the builder-shape single call
337
+ # (leptris_element_create_child). Tree semantics identical to
338
+ # document.create_element(name) + add_child.
339
+ def create_child(name)
340
+ ensure_writable!
341
+ ptr = Leptris::XML::FFI.leptris_element_create_child(@c_ptr, name.to_s)
342
+ raise Leptris::XML::Error,
343
+ "leptris_element_create_child failed for #{name.inspect}" if ptr.null?
344
+ Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
345
+ end
346
+
305
347
  def add_child(node_or_markup)
306
348
  ensure_writable!
307
349
  case node_or_markup
308
350
  when Leptris::XML::Node
351
+ Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
309
352
  Leptris::XML::FFI.check_status(
310
353
  Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
311
354
  node_or_markup
@@ -435,6 +435,8 @@ attach_function :leptris_parse_string,
435
435
 
436
436
  attach_function :leptris_element_create,
437
437
  [:leptris_document, :string], :leptris_element
438
+ attach_function :leptris_element_create_child,
439
+ [:leptris_element, :string], :leptris_element
438
440
  attach_function :leptris_element_set_name,
439
441
  [:leptris_element, :string], :leptris_status
440
442
  attach_function :leptris_element_set_text,
@@ -455,6 +457,10 @@ attach_function :leptris_parse_string,
455
457
  [:leptris_element, :string], :leptris_status
456
458
  attach_function :leptris_element_append_child,
457
459
  [:leptris_element, :leptris_element], :leptris_status
460
+ # Fused create+append (libleptris 1.9.153): one document
461
+ # resolution, identical tree semantics to the two-call pair.
462
+ attach_function :leptris_element_create_child,
463
+ [:leptris_element, :string], :leptris_element
458
464
  attach_function :leptris_element_prepend_child,
459
465
  [:leptris_element, :leptris_element], :leptris_status
460
466
  attach_function :leptris_element_insert_before,
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris::XML::Searchable
4
+ # Values that make a :version key the version channel (#183,
5
+ # mirroring leptris-py#105); anything else is a namespace prefix.
6
+ VERSION_SELECTORS = ["1.0", "3.1", :xpath10, :xpath31].freeze
7
+
4
8
  def xpath(*paths)
5
- handler, ns, _vars = parse_search_args(paths)
9
+ handler, ns, version = parse_search_args(paths)
6
10
  raise ArgumentError, "custom XPath handlers not supported" if handler
7
11
  expr = paths.join(" | ")
8
12
 
@@ -10,7 +14,16 @@ module Leptris::XML::Searchable
10
14
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
11
15
 
12
16
  result_ptr =
13
- if ns && !ns.empty?
17
+ if version
18
+ if ns && !ns.empty?
19
+ raise ArgumentError,
20
+ "version: cannot be combined with namespace bindings " \
21
+ "(the versioned engine entry takes no ns set yet)"
22
+ end
23
+ Leptris::XML::FFI.leptris_xpath_eval_versioned(
24
+ doc_ptr, context_ptr, expr,
25
+ Leptris::XML::Searchable.xpath_version_code(version), nil)
26
+ elsif ns && !ns.empty?
14
27
  xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
15
28
  else
16
29
  Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
@@ -24,7 +37,7 @@ module Leptris::XML::Searchable
24
37
  end
25
38
 
26
39
  def at_xpath(*paths)
27
- handler, ns, _vars = parse_search_args(paths)
40
+ handler, ns, version = parse_search_args(paths)
28
41
  raise ArgumentError, "custom XPath handlers not supported" if handler
29
42
  expr = paths.join(" | ")
30
43
 
@@ -32,7 +45,16 @@ module Leptris::XML::Searchable
32
45
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
33
46
 
34
47
  result_ptr =
35
- if ns && !ns.empty?
48
+ if version
49
+ if ns && !ns.empty?
50
+ raise ArgumentError,
51
+ "version: cannot be combined with namespace bindings " \
52
+ "(the versioned engine entry takes no ns set yet)"
53
+ end
54
+ Leptris::XML::FFI.leptris_xpath_eval_versioned(
55
+ doc_ptr, context_ptr, expr,
56
+ Leptris::XML::Searchable.xpath_version_code(version), nil)
57
+ elsif ns && !ns.empty?
36
58
  xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
37
59
  else
38
60
  Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
@@ -64,8 +86,9 @@ module Leptris::XML::Searchable
64
86
  alias_method :%, :at
65
87
 
66
88
  def css(*args)
67
- handler, ns, _ = parse_search_args(args)
89
+ handler, ns, version = parse_search_args(args)
68
90
  raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
91
+ raise ArgumentError, "version: is XPath-only" if version
69
92
  raise ArgumentError, "custom CSS handlers not supported" if handler
70
93
  # Nokogiri semantics: css is receiver-relative — absolute "//"
71
94
  # from a Document, descendant ".//" from elements and fragments.
@@ -76,8 +99,9 @@ module Leptris::XML::Searchable
76
99
  end
77
100
 
78
101
  def at_css(*args)
79
- handler, ns, _ = parse_search_args(args)
102
+ handler, ns, version = parse_search_args(args)
80
103
  raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
104
+ raise ArgumentError, "version: is XPath-only" if version
81
105
  raise ArgumentError, "custom CSS handlers not supported" if handler
82
106
  prefix = is_a?(Leptris::XML::Document) ? "//" : ".//"
83
107
  expr = args.map { |r| Leptris::XML::CssToXPath.convert(r, prefix: prefix) }
@@ -131,6 +155,15 @@ module Leptris::XML::Searchable
131
155
  end
132
156
  end
133
157
 
158
+ # Trailing-argument contract (#183): a String-keyed hash is the
159
+ # namespace-binding channel — as is a Symbol-keyed hash (the
160
+ # legacy prefix vocabulary, {x: "urn:x"}); a :version key whose
161
+ # VALUE is a version selector is the version-selection channel
162
+ # (xpath("//b", version: "1.0"), mirroring leptris-py#105). A
163
+ # DECLARED keyword cannot carry this: the moment #xpath declares
164
+ # one, Ruby converts the trailing namespace hash into keywords
165
+ # (unknown keyword: "p") — so both channels stay positional and
166
+ # dispatch on key/value here.
134
167
  def parse_search_args(args)
135
168
  handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
136
169
  args = args - [handler] if handler
@@ -143,7 +176,27 @@ module Leptris::XML::Searchable
143
176
  unless vars.nil? || vars.empty?
144
177
  raise ArgumentError, "XPath variable bindings are not supported"
145
178
  end
146
- [handler, ns, vars]
179
+ # Value-based disambiguation is load-bearing: Symbol keys are
180
+ # also the namespace vocabulary, and bare keywords FUSE into a
181
+ # preceding String-keyed hash at the call site ({"p" => uri,
182
+ # version: "1.0"} arrives as ONE hash). A :version key with any
183
+ # other value stays a prefix binding.
184
+ version = nil
185
+ if ns.is_a?(Hash)
186
+ v = ns[:version]
187
+ if VERSION_SELECTORS.include?(v)
188
+ version = ns.delete(:version)
189
+ ns = nil if ns.empty?
190
+ elsif v.is_a?(String) && v.match?(/\A\d+\.\d+\z/)
191
+ # A dotted-number :version value is version INTENT — raise
192
+ # with the valid values rather than silently binding a
193
+ # prefix literally. Genuine prefix bindings carry URIs.
194
+ raise ArgumentError,
195
+ "unknown XPath version #{v.inspect} " \
196
+ "(valid: \"1.0\"/:xpath10, \"3.1\"/:xpath31)"
197
+ end
198
+ end
199
+ [handler, ns, version]
147
200
  end
148
201
 
149
202
  def looks_like_xpath?(str)
@@ -18,28 +18,44 @@ class Leptris::XML::XPath
18
18
 
19
19
  attr_reader :expression
20
20
 
21
- def self.compile(expression)
21
+ # +version+ (#183, mirroring leptris-py#105): nil compiles the
22
+ # full-grammar expression (the default); :xpath10 / "1.0" pins
23
+ # the strict XPath 1.0 surface. Validated at the boundary.
24
+ def self.compile(expression, version: nil)
22
25
  raw = Leptris::XML::FFI.leptris_xpath_compile(expression.to_s)
23
26
  if raw.null?
24
27
  raise Leptris::XML::XPathError,
25
28
  "invalid expression: #{Leptris::XML::FFI.leptris_last_error}"
26
29
  end
27
- new(expression.to_s, CompiledHandle.new(raw))
30
+ version_code =
31
+ Leptris::XML::Searchable.xpath_version_code(version) if version
32
+ new(expression.to_s, CompiledHandle.new(raw), version_code)
28
33
  end
29
34
 
30
- def initialize(expression, handle)
35
+ def initialize(expression, handle, version_code = nil)
31
36
  @expression = expression
32
37
  @handle = handle
38
+ @version_code = version_code
33
39
  end
34
40
 
35
41
  # Evaluates against +doc_or_element+. An optional trailing hash of
36
42
  # namespace bindings ("prefix" => uri) routes the evaluation through
37
43
  # the namespace-bound path, matching Searchable#xpath semantics.
44
+ # A compiled version pin routes through the engine's versioned
45
+ # entry instead of the compiled handle — there is no compiled
46
+ # versioned lane yet (engine gap), so the strict-1.0 surface is
47
+ # enforced per evaluation over the stored expression.
38
48
  def eval(doc_or_element, ns = nil)
39
49
  context = Leptris::XML::EvaluationContext.of(doc_or_element)
40
50
  document = context.document
41
51
  result_ptr =
42
- if ns && !ns.empty?
52
+ if @version_code
53
+ raise ArgumentError,
54
+ "version: cannot be combined with namespace bindings " "(the versioned engine entry takes no ns set yet)" if ns && !ns.empty?
55
+ Leptris::XML::FFI.leptris_xpath_eval_versioned(
56
+ document.c_ptr, context.context_node_ptr,
57
+ @expression, @version_code, nil)
58
+ elsif ns && !ns.empty?
43
59
  Leptris::XML::FFI.with_ns_set(ns) do |set|
44
60
  Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
45
61
  @handle, document.c_ptr, context.context_node_ptr, set)
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.144.1
4
+ version: 1.9.156.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-09-12 00:00:00.000000000 Z
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi