leptris 1.9.34 → 1.9.35

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: f0445600493fa886524c162f270e55bccb3550d2c8b9463fe0a7ce305687a7ae
4
- data.tar.gz: 878c7ecdcb828d80e3a25c0a05932113ce33030ccf163aea3896453fa0fc0bb8
3
+ metadata.gz: 81d0ccc67839cd649fce63e998598f641e731492085d5fd09f5d6f5be226c743
4
+ data.tar.gz: f6914705968260382baa876b12f864d712544919a1186ef90cc0ba76857a8f86
5
5
  SHA512:
6
- metadata.gz: e9ab5ca5b140e729066e27a1afa5882639c7e18b6a10a94be06eccca5723ce0bf4547dd70940f367277548802caaca4a76d874148153fd55131ba5bb1d20aeb2
7
- data.tar.gz: c8c62d53ff231fb9eedf7f8aa5a3d96774517bf26b347cc4b833ee8d149d9c80a37e014b1f4833bd2a7074eae4944d13819d89c292c8a0ccf0ead4b147101ff2
6
+ metadata.gz: 70235b9639261a967137bc78694b0cbe447f8165c95819e7d28db24c4f37775fb67aca769e05462231ecd8bdf14f542a42cdd3b73159546c36232aaaf14fc848
7
+ data.tar.gz: 053d1e08b23283d82d83c7276417623358e4f29207eaf77fec9f71d319c4dc48ac5f6ad8d5ff038190d5a29b4e9956202abde97b2bb708a256837cbc5b67c21f
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.35] - 2026-08-28
9
+
10
+ ### Added
11
+
12
+ - **libleptris 1.9.9 lockstep**: pin bumped; symbol audit 249/249
13
+ with `leptris_document_remove_pi` and
14
+ `leptris_document_serialize_ext` attached. Engine fixes ride
15
+ along: CDATA `]]>` runs split across node boundaries, XSLT
16
+ suite 144 -> 152/205, attribute-axis entity expansion.
17
+ - **Document-level PIs fully mutable (leptris-ruby#92, upstream
18
+ #612)**: parse-created document-level PIs carry document
19
+ linkage — `target=`/`data=` work like on any tree PI (the
20
+ 1.9.32 contract-error mapping is removed). `PI#unlink` on a
21
+ document-level PI routes through the new document-level removal,
22
+ identity-matched by index so the right same-target PI comes out.
23
+ New `Document#remove_pi(target_or_index)` — by target or 0-based
24
+ index, returns the removed (pool-owned) PI or nil.
25
+ - **`Document#to_xml(indent_text: true)`** — display-form
26
+ serialization (upstream #129): text and mixed content indent;
27
+ output is display-oriented and not round-trip-guaranteed.
28
+
29
+ ### Changed
30
+
31
+ - **Inline memo on `Element#[]`**: the external battery caught
32
+ attribute reads ~1.6x behind Nokogiri — six method dispatches
33
+ per read through the attributes/memo_hit? tower. The guard is
34
+ now spelled inline over a values hash materialized alongside the
35
+ Attr objects (same ADR-0003 semantics, misses included).
36
+
37
+ ### Meta
38
+
39
+ - External head-to-head battery vs Nokogiri 1.19.4 (big.xml):
40
+ parse 12.3x, css 4.8x, xpath nodeset 3.0x, serialize 2.3x,
41
+ at_css 1.4x, SAX both shapes (round XXVI). Two shapes remain
42
+ behind and are C-side-bound: cold full walk 1.4x (needs
43
+ node_children out_kinds to skip per-node get_type — filing
44
+ upstream) and scalar xpath 1.5x (engine-side scalar eval).
45
+
8
46
  ## [1.9.34] - 2026-08-28
9
47
 
10
48
  ### Changed
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.8"
11
+ LIBLEPTRIS_VERSION = "1.9.9"
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.34"
4
+ VERSION = "1.9.35"
5
5
  end
@@ -266,9 +266,16 @@ class Leptris::XML::Document
266
266
  end
267
267
  alias_method :internal_subset, :doctype
268
268
 
269
- def to_xml(indent: 0, no_decl: false, encoding: nil)
269
+ # indent_text: true selects the display form (libleptris 1.9.9,
270
+ # #129): text and mixed content indent too — the output is
271
+ # display-oriented and not round-trip-guaranteed.
272
+ def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false)
270
273
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
271
274
  return "" if @c_ptr.nil?
275
+ if indent_text
276
+ return Leptris::XML::Serialization.to_xml_display(
277
+ @c_ptr, indent: indent, no_decl: no_decl, encoding: encoding)
278
+ end
272
279
  Leptris::XML::Serialization.to_xml(
273
280
  Leptris::XML::FFI.method(:leptris_document_serialize_into), @c_ptr,
274
281
  indent: indent, no_decl: no_decl, encoding: encoding)
@@ -338,6 +345,25 @@ class Leptris::XML::Document
338
345
  result
339
346
  end
340
347
 
348
+ # Removes a document-level processing instruction — by target
349
+ # (String/Symbol) or by 0-based index among the document's PIs
350
+ # (Integer) (libleptris 1.9.9, upstream #612). Returns the removed
351
+ # PI — pool-owned and valid until #free — or nil when nothing
352
+ # matched.
353
+ def remove_pi(target_or_index)
354
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
355
+ if target_or_index.is_a?(Integer)
356
+ ptr = Leptris::XML::FFI.leptris_document_remove_pi(
357
+ @c_ptr, nil, target_or_index)
358
+ else
359
+ ptr = Leptris::XML::FFI.leptris_document_remove_pi(
360
+ @c_ptr, target_or_index.to_s, 0)
361
+ end
362
+ return nil if ptr.null?
363
+ @version += 1
364
+ Leptris::XML::Node.wrap(ptr, self)
365
+ end
366
+
341
367
  # Append a document-level processing instruction. Returns self.
342
368
  def add_pi(target, data = "")
343
369
  witness = Leptris::XML::FFI.leptris_document_add_pi(
@@ -44,8 +44,18 @@ class Leptris::XML::Element < Leptris::XML::Node
44
44
  # written prefix never matters — the hash cannot answer them.
45
45
  name = key.to_s
46
46
  if @document && !name.include?(":")
47
- cached = attributes[name]
48
- return cached.nil? ? nil : cached.value
47
+ # The hottest read in the library: the memo guard is spelled
48
+ # INLINE rather than through attributes/#memo_hit? six
49
+ # method dispatches per read was ~40% of the read cost, and
50
+ # the external head-to-head (nokogiri's one C call) was
51
+ # winning on it. Same three-line memo semantics (ADR 0003),
52
+ # incl. the nil-miss — the hash answers misses too.
53
+ values = @attr_values
54
+ unless values && @attributes_version == @document.version
55
+ attributes
56
+ values = @attr_values
57
+ end
58
+ return values[name]
49
59
  end
50
60
  ensure_alive!
51
61
  Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
@@ -133,9 +143,14 @@ class Leptris::XML::Element < Leptris::XML::Node
133
143
  def attributes
134
144
  return @attributes if memo_hit?(@attributes_version)
135
145
  result = {}
136
- each_attribute { |attr| result[attr.name] = attr }
146
+ values = {}
147
+ each_attribute do |attr|
148
+ result[attr.name] = attr
149
+ values[attr.name] = attr.value
150
+ end
137
151
  if @document
138
152
  @attributes = result
153
+ @attr_values = values
139
154
  @attributes_version = @document.version
140
155
  end
141
156
  result
@@ -72,6 +72,12 @@ module Leptris
72
72
  :encoding, :pointer
73
73
  end
74
74
 
75
+ # LeptrisSerializeExtOptions (1.9.9, #129): display-form
76
+ # serialization knobs beyond the ABI-frozen options struct.
77
+ class SerializeExtStruct < ::FFI::Struct
78
+ layout :indent_text, :int
79
+ end
80
+
75
81
  # Mirrors LeptrisParseOptions (libleptris >= 1.9.0 carries the
76
82
  # recover field; earlier fields unchanged since v1.6.0).
77
83
  class ParseOptionsStruct < ::FFI::Struct
@@ -156,6 +162,11 @@ module Leptris
156
162
  [:leptris_document, :size_t], :string
157
163
  attach_function :leptris_document_pi_data,
158
164
  [:leptris_document, :size_t], :string
165
+ # Document-level PI removal by target or index (1.9.9, #612):
166
+ # unlinks from the child chain; the node is pool-owned and
167
+ # stays valid until document free.
168
+ attach_function :leptris_document_remove_pi,
169
+ [:leptris_document, :string, :size_t], :leptris_node_ref
159
170
  attach_function :leptris_document_add_pi,
160
171
  [:leptris_document, :string, :string], :pointer
161
172
  # Document-level comments (libleptris 1.9.3, #578): parsed
@@ -611,6 +622,11 @@ module Leptris
611
622
  # (leptris#541). buf=NULL is a size query; the size-query +
612
623
  # fill pair reuses one serialization through a per-document
613
624
  # cache invalidated on mutation.
625
+ # Display-form variant (1.9.9, #129): the ext struct carries
626
+ # indent_text; returns an OWNED string — read via
627
+ # read_owned_string (the seam frees it).
628
+ attach_function :leptris_document_serialize_ext,
629
+ [:leptris_document, :pointer, :pointer], :pointer
614
630
  attach_function :leptris_document_serialize_into,
615
631
  [:leptris_document, :pointer, :size_t, :pointer, :pointer], :size_t
616
632
  attach_function :leptris_document_get_dtd,
@@ -263,12 +263,8 @@ class Leptris::XML::Node
263
263
 
264
264
  def unlink
265
265
  ensure_writable!
266
- rc = Leptris::XML::FFI.leptris_node_unlink(@c_ptr)
267
- if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND && processing_instruction?
268
- raise Leptris::XML::Error,
269
- Leptris::XML::ProcessingInstruction::READ_ONLY_NATIVE
270
- end
271
- Leptris::XML::FFI.check_status(rc)
266
+ Leptris::XML::FFI.check_status(
267
+ Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
272
268
  @parent = nil
273
269
  self
274
270
  end
@@ -25,34 +25,58 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
25
25
  result
26
26
  end
27
27
 
28
- # Parse-created document-level PIs carry no document linkage in
29
- # libleptris (leptris-ruby#92): the C setters reject them with
30
- # INVALID_ARG while tree-level and Document#add_pi PIs mutate
31
- # fine. Map that failure to the contract instead of a bare
32
- # "Invalid argument".
33
- READ_ONLY_NATIVE =
34
- "this PI is a parse-created document-level PI — its node has " \
35
- "no document linkage in libleptris, so target/data cannot be " \
36
- "written and it cannot be unlinked (leptris-ruby#92); " \
37
- "tree-level and Document#add_pi PIs are mutable"
38
-
28
+ # Since libleptris 1.9.9 (#612) parse-created document-level PIs
29
+ # carry document linkage — the setters work on them like on any
30
+ # tree-level PI.
39
31
  def target=(new_target)
40
32
  ensure_writable!
41
- rc = Leptris::XML::FFI.leptris_pi_node_set_target(
42
- @c_ptr, new_target.to_s)
43
- raise Leptris::XML::Error, READ_ONLY_NATIVE if
44
- rc == Leptris::XML::FFI::LEPTRIS_ERROR_INVALID_ARG
45
- Leptris::XML::FFI.check_status(rc)
33
+ Leptris::XML::FFI.check_status(
34
+ Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s))
46
35
  new_target
47
36
  end
48
37
 
49
38
  def data=(new_data)
50
39
  ensure_writable!
51
- rc = Leptris::XML::FFI.leptris_pi_node_set_data(
52
- @c_ptr, new_data.to_s)
53
- raise Leptris::XML::Error, READ_ONLY_NATIVE if
54
- rc == Leptris::XML::FFI::LEPTRIS_ERROR_INVALID_ARG
55
- Leptris::XML::FFI.check_status(rc)
40
+ Leptris::XML::FFI.check_status(
41
+ Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s))
56
42
  new_data
57
43
  end
44
+
45
+ # Document-level PIs have no tree parent for leptris_node_unlink
46
+ # — route through the document-level removal (libleptris 1.9.9,
47
+ # #612), identity-matched by index so the right same-target PI
48
+ # comes out.
49
+ def unlink
50
+ ensure_writable!
51
+ rc = Leptris::XML::FFI.leptris_node_unlink(@c_ptr)
52
+ if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND
53
+ index = document_pi_index
54
+ if index.negative?
55
+ raise Leptris::XML::Error,
56
+ "PI not found in its document — already removed?"
57
+ end
58
+ removed = @document.remove_pi(index)
59
+ unless removed && removed.c_ptr == @c_ptr
60
+ raise Leptris::XML::Error, "document-level PI removal missed"
61
+ end
62
+ @parent = nil
63
+ return self
64
+ end
65
+ Leptris::XML::FFI.check_status(rc)
66
+ @parent = nil
67
+ self
68
+ end
69
+
70
+ private
71
+
72
+ # This PI's 0-based index among the document's PIs (document
73
+ # order — the same enumeration leptris_document_remove_pi uses).
74
+ def document_pi_index
75
+ pi_index = -1
76
+ @document.children.each do |child|
77
+ pi_index += 1 if child.pi?
78
+ return pi_index if child.c_ptr == @c_ptr
79
+ end
80
+ -1
81
+ end
58
82
  end
@@ -33,6 +33,25 @@ module Leptris::XML::Serialization
33
33
  Leptris::XML::FFI.serialize_into_string(ffi_function, c_ptr, opts.pointer)
34
34
  end
35
35
 
36
+ # Display-form document serialization (libleptris 1.9.9, #129):
37
+ # the ext struct's indent_text hands ALL whitespace to the
38
+ # formatter — text and mixed content indent too. Display-oriented:
39
+ # the output is NOT guaranteed to round-trip byte-exactly.
40
+ INDENT_TEXT_EXT = begin
41
+ ext = Leptris::XML::FFI::SerializeExtStruct.new
42
+ ext[:indent_text] = 1
43
+ ext
44
+ end
45
+ private_constant :INDENT_TEXT_EXT
46
+
47
+ def self.to_xml_display(c_ptr, indent: 0, no_decl: false, encoding: nil)
48
+ opts, encoding_anchor = build_options(
49
+ indent: indent, no_decl: no_decl, encoding: encoding)
50
+ str_ptr = Leptris::XML::FFI.leptris_document_serialize_ext(
51
+ c_ptr, opts.pointer, INDENT_TEXT_EXT.pointer)
52
+ Leptris::XML::FFI.read_owned_string(str_ptr)
53
+ end
54
+
36
55
  # +ffi_function+ is a bound FFI function taking
37
56
  # (c_ptr, version, mode, ns_ptr, flags): leptris_c14n_canonicalize_ex
38
57
  # or leptris_c14n_canonicalize_subtree_ex.
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.34
4
+ version: 1.9.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.