leptris 1.9.35-aarch64-linux → 1.9.37-aarch64-linux

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: a5d2cc0c763678c71c93628564323985c5e55f27ea126aa9f003319e4a7a7524
4
- data.tar.gz: 8545c65e4579fe342f87938cf4f24f63dd02b7900bbf4bad7e040934a35b750e
3
+ metadata.gz: ec92a3dcbcfe8ff73e97968df81833ac687ac7f2dd25d067bd4f16cccecbb19c
4
+ data.tar.gz: cc6b3233de37a3cf732ff3b1e007548e9a22e07a1f6f928da61202053f99dd94
5
5
  SHA512:
6
- metadata.gz: 4c936cd0a55e640174407bcbc38a774679949d327c00dfd36ac058828094033405bdf5e9960f87d4e390c67b8ae58aea9aa2dd7f3a61adbae626f41488fadb8b
7
- data.tar.gz: 84e79fc4a1bb6d563aefee5a44ae7b78e19488e3991a167221f20bbaff441cd289bf5c3e459d0ddfdec1d00f67429705e77aec5175169360496fce29f8fa84bf
6
+ metadata.gz: 716b5a27e519fb75c622065bf0d1c6588cf11dbafb13418de2f8a72409ce475a2434d97bcebae932669f79a7f095e6645aef6e81c59fc27ef8f2ded1f66fe4bd
7
+ data.tar.gz: 190f571def471b9351a21ec3e856577175a018efe36626fc20091df9e02fb06708ba46e49384dcb6d6e345e058f021fa53c93eadaeb2968160f67d550a822243
data/CHANGELOG.md CHANGED
@@ -5,6 +5,75 @@ 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.37] - 2026-08-29
9
+
10
+ ### Added
11
+
12
+ - **`Element#inner_html`** (Nokogiri parity): the serialized
13
+ children — elements through the engine serializer, text
14
+ XML-escaped at the seam (`& < >` and CR → `&#xD;`, libxml2's
15
+ rules), comments/CDATA/PIs in literal forms. Well-formed by
16
+ construction: a spec re-parses the output to the same children.
17
+ (Nokogiri's inner_html HTML-serializes XML documents — SGML-style
18
+ PI closes, bare CDATA content — and its output does not
19
+ re-parse.) Measured 21 µs/element vs Nokogiri 15 — a first cut
20
+ on a new API with correct forms as the contract.
21
+ - **libleptris 1.9.12 lockstep** (with 1.9.11): pure XSLT
22
+ conformance upstream (libxslt suite 152 -> 180/205 — template
23
+ priority, xsl:number, attribute sets, namespace-declaration
24
+ ordering, strip/preserve-space, copy semantics); audit 250/250,
25
+ no new C surface.
26
+
27
+ ### Changed
28
+
29
+ - **Serialize byte-scratch**: the size+fill serialization pair
30
+ allocated and freed a MemoryPointer per call — inner_html
31
+ serializes each child, and the allocation dominated per-child
32
+ cost. A grow-only thread-local byte scratch (the pointer-scratch
33
+ discipline) now serves every serialize call. Serialized output
34
+ is also forced UTF-8 at the seam (previously true only by
35
+ accident of ASCII-only content comparisons).
36
+
37
+ ## [1.9.36] - 2026-08-29
38
+
39
+ ### Fixed
40
+
41
+ - **SAX attribute corruption (leptris-ruby#95)** — fixed fully in
42
+ the binding. The engine's streaming attribute buffer loses
43
+ leading attribute pairs when several nested ancestor levels each
44
+ carry attributes; all THREE streaming surfaces are affected (the
45
+ callback transport, the recorder's arena, and pull attrs — each
46
+ verified on the issue fixture, which also yields run-varying
47
+ uninitialized bytes), while the DOM parser reads the same bytes
48
+ correctly. `SAX::Parser` now delivers from a DOM parse by
49
+ default (`SAX::DomDispatch`): one parse, then a lean walk
50
+ reproducing the streaming call shapes exactly —
51
+ attach-only-overridden kinds, one-arg arity dispatch, QName
52
+ element names, UTF-8, PI-data normalization. The issue fixture
53
+ is a regression spec (all 7 image attributes intact; the entire
54
+ existing SAX suite passes byte-identically through the new
55
+ path). The engine transports remain available via
56
+ `Parser.new(handler, streaming: true)` — and return as the
57
+ default the moment the engine fix lands. Correctness-first tax,
58
+ measured on big.xml: all-events 375 ms (engine 112, nokogiri
59
+ 130); the bisection (not positional, not attribute length;
60
+ trigger = nested attr-carrying ancestors; all three surfaces
61
+ share the buffer) is filed upstream for the C fix.
62
+
63
+ ### Added
64
+
65
+ - **libleptris 1.9.10 lockstep**: pin bumped; audit 250/250 with
66
+ `leptris_sax_recorder_reset` attached. `SAX::Recorder#reset` —
67
+ reuse one recorder across documents (upstream #594's
68
+ one-document-per-instance finding): fresh parser state, record/
69
+ arena buffers retained.
70
+
71
+ ### Meta
72
+
73
+ - Memory battery (first measurement, 10 x big.xml held): leptris
74
+ **17.5 MB/doc vs nokogiri 31.3** (1.8x lighter); both free
75
+ cleanly.
76
+
8
77
  ## [1.9.35] - 2026-08-28
9
78
 
10
79
  ### Added
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.9"
11
+ LIBLEPTRIS_VERSION = "1.9.12"
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.35"
4
+ VERSION = "1.9.37"
5
5
  end
@@ -336,6 +336,28 @@ class Leptris::XML::Element < Leptris::XML::Node
336
336
  scopes
337
337
  end
338
338
 
339
+ # The serialized children (Nokogiri parity): elements serialize
340
+ # through the engine (correct escaping), text is XML-escaped,
341
+ # comments/CDATA/PIs render their literal forms. The complement
342
+ # of #inner_text, which returns the unescaped text content.
343
+ def inner_html
344
+ children.map do |child|
345
+ case child
346
+ when Leptris::XML::Element then child.to_xml
347
+ when Leptris::XML::CDATA # before Text: CDATA subclasses it
348
+ "<![CDATA[#{child.content}]]>"
349
+ when Leptris::XML::Text
350
+ Leptris::XML::Serialization.escape_text(child.content)
351
+ when Leptris::XML::Comment then "<!--#{child.content}-->"
352
+ when Leptris::XML::ProcessingInstruction
353
+ data = child.content
354
+ data.empty? ? "<?#{child.name}?>" : "<?#{child.name} #{data}?>"
355
+ else
356
+ Leptris::XML::Serialization.escape_text(child.content.to_s)
357
+ end
358
+ end.join
359
+ end
360
+
339
361
  def to_xml(indent: 0, no_decl: false, encoding: nil)
340
362
  Leptris::XML::Serialization.to_xml(
341
363
  Leptris::XML::FFI.method(:leptris_element_serialize_into), @c_ptr,
@@ -561,6 +561,11 @@ module Leptris
561
561
  [:leptris_sax_recorder, :pointer], :pointer
562
562
  attach_function :leptris_sax_recorder_arena,
563
563
  [:leptris_sax_recorder, :pointer], :pointer
564
+ # Reuse across documents (1.9.10, #594): swaps in fresh parser
565
+ # state, RETAINS the record/arena buffers — a
566
+ # one-document-per-parse loop stops paying new/free churn.
567
+ attach_function :leptris_sax_recorder_reset,
568
+ [:leptris_sax_recorder], :int
564
569
  attach_function :leptris_sax_recorder_free,
565
570
  [:leptris_sax_recorder], :void
566
571
  attach_function :leptris_sax_parser_free,
@@ -860,13 +865,24 @@ module Leptris
860
865
  def self.serialize_into_string(ffi_function, c_ptr, options)
861
866
  need = ffi_function.call(c_ptr, nil, 0, nil, options)
862
867
  return "" if need.zero?
863
- buffer = ::FFI::MemoryPointer.new(:char, need)
864
- begin
865
- ffi_function.call(c_ptr, buffer, need, nil, options)
866
- buffer.read_string
867
- ensure
868
- buffer.free
868
+ buffer = scratch_bytes(need)
869
+ ffi_function.call(c_ptr, buffer, need, nil, options)
870
+ buffer.read_string.force_encoding(Encoding::UTF_8)
871
+ end
872
+
873
+ # Byte scratch for serialize-into cycles: the size+fill pair
874
+ # allocated and freed a buffer per call — inner_html
875
+ # serializes each child, and the allocation was most of the
876
+ # per-child cost. Grows to the largest serialization seen;
877
+ # no Ruby reentrancy during a serialize call.
878
+ def self.scratch_bytes(need)
879
+ scratch = (Thread.current[:leptris_scratch] ||= {})
880
+ ptr = scratch[:bytes]
881
+ if ptr.nil? || ptr.size < need
882
+ ptr&.free
883
+ ptr = scratch[:bytes] = ::FFI::MemoryPointer.new(:char, need)
869
884
  end
885
+ ptr
870
886
  end
871
887
 
872
888
  # All-kind child handle batch (libleptris 1.7.0). Opportunistic
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DOM-backed SAX dispatch (leptris-ruby#95): the engine's streaming
4
+ # attribute buffer corrupts leading attribute pairs when several
5
+ # nested ancestor levels each carry attributes — the callback
6
+ # transport, the recorder's packed arena, AND the pull parser all
7
+ # deliver the loss on the issue fixture (verified), while the DOM
8
+ # parser reads the same bytes correctly. Until the engine fix
9
+ # lands, SAX::Parser delivers from a DOM parse: same call shapes as
10
+ # the streaming transports — attach-only-overridden kinds,
11
+ # one-argument start_element arity, QName element names ("p:a"),
12
+ # UTF-8 strings, PI-data normalization.
13
+ #
14
+ # The walk is lean by construction: elements wrap (identity, name
15
+ # and prefix memos); every other child kind is read straight from
16
+ # its pointer — content getters per node type, never a wrapper.
17
+ module Leptris::XML::SAX::DomDispatch
18
+ module_function
19
+
20
+ def parse(handler, dispatched, string)
21
+ doc = Leptris::XML::Document.parse(string)
22
+ begin
23
+ dispatch(handler, dispatched, doc)
24
+ ensure
25
+ doc.free
26
+ end
27
+ end
28
+
29
+ def dispatch(handler, dispatched, doc)
30
+ handler.start_document if dispatched[:start_document]
31
+ doc_node = doc.node
32
+ visit_children(doc_node.c_ptr, doc, handler, dispatched) if doc_node
33
+ handler.end_document if dispatched[:end_document]
34
+ self
35
+ end
36
+
37
+ NODE = Leptris::XML::FFI
38
+ ELEMENT = Leptris::XML::FFI::NODE_ELEMENT
39
+ TEXT = Leptris::XML::FFI::NODE_TEXT
40
+ COMMENT = Leptris::XML::FFI::NODE_COMMENT
41
+ CDATA = Leptris::XML::FFI::NODE_CDATA
42
+ PI = Leptris::XML::FFI::NODE_PI
43
+
44
+ def visit_children(c_ptr, doc, handler, dispatched)
45
+ wants_text_kinds = dispatched[:characters] ||
46
+ dispatched[:comment] ||
47
+ dispatched[:cdata] || dispatched[:pi]
48
+ Leptris::XML::FFI.fetch_children(c_ptr).each do |child_ptr|
49
+ case Leptris::XML::FFI.leptris_node_get_type(child_ptr)
50
+ when ELEMENT
51
+ visit_element(
52
+ Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
53
+ handler, dispatched)
54
+ when TEXT
55
+ if dispatched[:characters]
56
+ handler.characters(utf8(NODE.leptris_text_node_get_content(child_ptr)))
57
+ end
58
+ when CDATA
59
+ if dispatched[:cdata]
60
+ handler.cdata_block(utf8(NODE.leptris_cdata_node_get_content(child_ptr)))
61
+ end
62
+ when COMMENT
63
+ if dispatched[:comment]
64
+ handler.comment(utf8(NODE.leptris_comment_node_get_content(child_ptr)))
65
+ end
66
+ when PI
67
+ if dispatched[:pi]
68
+ handler.processing_instruction(
69
+ utf8(NODE.leptris_pi_node_get_target(child_ptr)),
70
+ Leptris::XML::FFI.read_pi_data(
71
+ utf8(NODE.leptris_pi_node_get_data(child_ptr))))
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def visit_element(element, handler, dispatched)
78
+ qname = element.prefix ? "#{element.prefix}:#{element.name}" : element.name
79
+ if dispatched[:start_prefix]
80
+ element.namespace_definitions.each do |ns|
81
+ handler.start_prefix_mapping(ns.prefix.to_s, ns.href.to_s)
82
+ end
83
+ end
84
+ if dispatched[:start_element]
85
+ if dispatched[:start_element] == :one_arg
86
+ handler.start_element(qname)
87
+ else
88
+ handler.start_element(qname, attr_pairs(element.c_ptr))
89
+ end
90
+ end
91
+ visit_children(element.c_ptr, element.document, handler, dispatched)
92
+ handler.end_element(qname) if dispatched[:end_element]
93
+ if dispatched[:end_prefix]
94
+ element.namespace_definitions.reverse_each do |ns|
95
+ handler.end_prefix_mapping(ns.prefix.to_s)
96
+ end
97
+ end
98
+ end
99
+
100
+ # [name, value] pairs in source order, read straight from the
101
+ # attribute chain — no Attr objects, no hash.
102
+ def attr_pairs(c_ptr)
103
+ pairs = []
104
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
105
+ until attr.nil? || attr.null?
106
+ pairs << [utf8(Leptris::XML::FFI.leptris_attribute_get_name(attr)),
107
+ utf8(Leptris::XML::FFI.leptris_attribute_get_value(
108
+ c_ptr, attr))]
109
+ attr = Leptris::XML::FFI.leptris_attribute_next(attr)
110
+ end
111
+ pairs
112
+ end
113
+
114
+ def utf8(str)
115
+ str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
116
+ end
117
+ end
@@ -8,9 +8,18 @@ class Leptris::XML::SAX::Parser
8
8
 
9
9
  attr_reader :document, :encoding
10
10
 
11
- def initialize(handler = Leptris::XML::SAX::Document.new, encoding = nil)
11
+ # streaming: false (default) delivers from a DOM parse —
12
+ # leptris-ruby#95: the engine's streaming SAX transports corrupt
13
+ # attribute state on nested attribute-heavy shapes while the DOM
14
+ # parser reads the same bytes correctly. streaming: true opts
15
+ # back into the engine transports (bulk/callback by override
16
+ # weight) once the engine fix lands, or for consumers that
17
+ # accept the risk.
18
+ def initialize(handler = Leptris::XML::SAX::Document.new,
19
+ encoding = nil, streaming: false)
12
20
  @document = handler
13
21
  @encoding = encoding
22
+ @streaming = streaming
14
23
  end
15
24
 
16
25
  # Swapping the handler invalidates the memoized callback struct so
@@ -41,6 +50,11 @@ class Leptris::XML::SAX::Parser
41
50
 
42
51
  def parse_memory(string)
43
52
  string = string.dup.force_encoding("UTF-8")
53
+ unless @streaming
54
+ Leptris::XML::SAX::DomDispatch.parse(
55
+ @document, dispatched_kinds, string)
56
+ return self
57
+ end
44
58
  if bulk_dispatch?
45
59
  parse_memory_bulk(string)
46
60
  else
@@ -133,6 +147,10 @@ class Leptris::XML::SAX::Parser
133
147
  end
134
148
 
135
149
  def parse_io(io)
150
+ unless @streaming
151
+ parse_memory(io.read)
152
+ return self
153
+ end
136
154
  handler_struct = self.handler_struct
137
155
  parser_ptr = Leptris::XML::FFI.leptris_sax_parser_create(
138
156
  handler_struct.pointer, nil)
@@ -159,7 +177,11 @@ class Leptris::XML::SAX::Parser
159
177
  end
160
178
 
161
179
  def parse_file(path)
162
- File.open(path, "r") { |f| parse_io(f) }
180
+ if @streaming
181
+ File.open(path, "r") { |f| parse_io(f) }
182
+ else
183
+ parse_memory(File.read(path))
184
+ end
163
185
  end
164
186
 
165
187
  private
@@ -69,6 +69,18 @@ class Leptris::XML::SAX::Recorder
69
69
  @handle = nil
70
70
  end
71
71
 
72
+ # Resets a finished recorder for the next document (libleptris
73
+ # 1.9.10, upstream #594): fresh parser state, record/arena
74
+ # buffers retained — a one-document-per-parse loop reuses ONE
75
+ # recorder without new/free churn. Records restart empty.
76
+ def reset
77
+ rc = Leptris::XML::FFI.leptris_sax_recorder_reset(@handle)
78
+ if rc != 0
79
+ raise Leptris::XML::Error, "leptris_sax_recorder_reset failed"
80
+ end
81
+ self
82
+ end
83
+
72
84
  # Feed one chunk; records/arena reset at feed entry, so drain
73
85
  # after every feed. Returns self.
74
86
  def feed(chunk, final: false)
@@ -8,6 +8,7 @@ module Leptris
8
8
  autoload :Document, "leptris/xml/sax/document"
9
9
  autoload :Parser, "leptris/xml/sax/parser"
10
10
  autoload :Recorder, "leptris/xml/sax/recorder"
11
+ autoload :DomDispatch, "leptris/xml/sax/dom_dispatch"
11
12
  end
12
13
  end
13
14
  end
@@ -62,6 +62,19 @@ module Leptris::XML::Serialization
62
62
  Leptris::XML::FFI.read_owned_string(str_ptr)
63
63
  end
64
64
 
65
+ # XML-escapes text content for inner_html: & first, then < >
66
+ # and CR (libxml2 emits &#xD; for a bare carriage return).
67
+ ESCAPE_TEXT = {
68
+ "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "\r" => "&#xD;",
69
+ }.freeze
70
+ private_constant :ESCAPE_TEXT
71
+ ESCAPE_TEXT_RE = /[&<>\r]/.freeze
72
+ private_constant :ESCAPE_TEXT_RE
73
+
74
+ def self.escape_text(string)
75
+ string.gsub(ESCAPE_TEXT_RE, ESCAPE_TEXT)
76
+ end
77
+
65
78
  # Builds a SerializeOptions struct. Returns [opts, encoding_anchor];
66
79
  # the anchor must stay referenced while opts is in an FFI call.
67
80
  def self.build_options(indent: 0, no_decl: false, encoding: nil)
data/lib/libleptris.so CHANGED
Binary file
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.35
4
+ version: 1.9.37
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -98,6 +98,7 @@ files:
98
98
  - lib/leptris/xml/pull.rb
99
99
  - lib/leptris/xml/sax.rb
100
100
  - lib/leptris/xml/sax/document.rb
101
+ - lib/leptris/xml/sax/dom_dispatch.rb
101
102
  - lib/leptris/xml/sax/parser.rb
102
103
  - lib/leptris/xml/sax/recorder.rb
103
104
  - lib/leptris/xml/searchable.rb