leptris 1.9.34 → 1.9.36

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: 31fdbaa742e583a50409dea1a5bda76e454e3b54ec30429d74101d5438f91b6f
4
+ data.tar.gz: 6acd554905283d755febf5c6265ce2a97dd8f9ce1ef83c9804363ff2d5bf1000
5
5
  SHA512:
6
- metadata.gz: e9ab5ca5b140e729066e27a1afa5882639c7e18b6a10a94be06eccca5723ce0bf4547dd70940f367277548802caaca4a76d874148153fd55131ba5bb1d20aeb2
7
- data.tar.gz: c8c62d53ff231fb9eedf7f8aa5a3d96774517bf26b347cc4b833ee8d149d9c80a37e014b1f4833bd2a7074eae4944d13819d89c292c8a0ccf0ead4b147101ff2
6
+ metadata.gz: 7e4c9abd84b07a678394774470b1486ccb2e03146578cda7eded0da2aa42f0b33f3ae682ac5a1881d2661e4a90ddb3bd6e0b3098d411303268867a2bbc1ad0d5
7
+ data.tar.gz: 35a2088c31974f7dc19beaa0bd4ad5c73515be100b1c0d3b3fd3bc313fa10e7abc28202f3eb7bb3cce6035ad86b0c3f553bde0ca458e08a03110ba0d774ff07a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,84 @@ 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.36] - 2026-08-29
9
+
10
+ ### Fixed
11
+
12
+ - **SAX attribute corruption (leptris-ruby#95)** — fixed fully in
13
+ the binding. The engine's streaming attribute buffer loses
14
+ leading attribute pairs when several nested ancestor levels each
15
+ carry attributes; all THREE streaming surfaces are affected (the
16
+ callback transport, the recorder's arena, and pull attrs — each
17
+ verified on the issue fixture, which also yields run-varying
18
+ uninitialized bytes), while the DOM parser reads the same bytes
19
+ correctly. `SAX::Parser` now delivers from a DOM parse by
20
+ default (`SAX::DomDispatch`): one parse, then a lean walk
21
+ reproducing the streaming call shapes exactly —
22
+ attach-only-overridden kinds, one-arg arity dispatch, QName
23
+ element names, UTF-8, PI-data normalization. The issue fixture
24
+ is a regression spec (all 7 image attributes intact; the entire
25
+ existing SAX suite passes byte-identically through the new
26
+ path). The engine transports remain available via
27
+ `Parser.new(handler, streaming: true)` — and return as the
28
+ default the moment the engine fix lands. Correctness-first tax,
29
+ measured on big.xml: all-events 375 ms (engine 112, nokogiri
30
+ 130); the bisection (not positional, not attribute length;
31
+ trigger = nested attr-carrying ancestors; all three surfaces
32
+ share the buffer) is filed upstream for the C fix.
33
+
34
+ ### Added
35
+
36
+ - **libleptris 1.9.10 lockstep**: pin bumped; audit 250/250 with
37
+ `leptris_sax_recorder_reset` attached. `SAX::Recorder#reset` —
38
+ reuse one recorder across documents (upstream #594's
39
+ one-document-per-instance finding): fresh parser state, record/
40
+ arena buffers retained.
41
+
42
+ ### Meta
43
+
44
+ - Memory battery (first measurement, 10 x big.xml held): leptris
45
+ **17.5 MB/doc vs nokogiri 31.3** (1.8x lighter); both free
46
+ cleanly.
47
+
48
+ ## [1.9.35] - 2026-08-28
49
+
50
+ ### Added
51
+
52
+ - **libleptris 1.9.9 lockstep**: pin bumped; symbol audit 249/249
53
+ with `leptris_document_remove_pi` and
54
+ `leptris_document_serialize_ext` attached. Engine fixes ride
55
+ along: CDATA `]]>` runs split across node boundaries, XSLT
56
+ suite 144 -> 152/205, attribute-axis entity expansion.
57
+ - **Document-level PIs fully mutable (leptris-ruby#92, upstream
58
+ #612)**: parse-created document-level PIs carry document
59
+ linkage — `target=`/`data=` work like on any tree PI (the
60
+ 1.9.32 contract-error mapping is removed). `PI#unlink` on a
61
+ document-level PI routes through the new document-level removal,
62
+ identity-matched by index so the right same-target PI comes out.
63
+ New `Document#remove_pi(target_or_index)` — by target or 0-based
64
+ index, returns the removed (pool-owned) PI or nil.
65
+ - **`Document#to_xml(indent_text: true)`** — display-form
66
+ serialization (upstream #129): text and mixed content indent;
67
+ output is display-oriented and not round-trip-guaranteed.
68
+
69
+ ### Changed
70
+
71
+ - **Inline memo on `Element#[]`**: the external battery caught
72
+ attribute reads ~1.6x behind Nokogiri — six method dispatches
73
+ per read through the attributes/memo_hit? tower. The guard is
74
+ now spelled inline over a values hash materialized alongside the
75
+ Attr objects (same ADR-0003 semantics, misses included).
76
+
77
+ ### Meta
78
+
79
+ - External head-to-head battery vs Nokogiri 1.19.4 (big.xml):
80
+ parse 12.3x, css 4.8x, xpath nodeset 3.0x, serialize 2.3x,
81
+ at_css 1.4x, SAX both shapes (round XXVI). Two shapes remain
82
+ behind and are C-side-bound: cold full walk 1.4x (needs
83
+ node_children out_kinds to skip per-node get_type — filing
84
+ upstream) and scalar xpath 1.5x (engine-side scalar eval).
85
+
8
86
  ## [1.9.34] - 2026-08-28
9
87
 
10
88
  ### 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.10"
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.36"
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
@@ -550,6 +561,11 @@ module Leptris
550
561
  [:leptris_sax_recorder, :pointer], :pointer
551
562
  attach_function :leptris_sax_recorder_arena,
552
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
553
569
  attach_function :leptris_sax_recorder_free,
554
570
  [:leptris_sax_recorder], :void
555
571
  attach_function :leptris_sax_parser_free,
@@ -611,6 +627,11 @@ module Leptris
611
627
  # (leptris#541). buf=NULL is a size query; the size-query +
612
628
  # fill pair reuses one serialization through a per-document
613
629
  # cache invalidated on mutation.
630
+ # Display-form variant (1.9.9, #129): the ext struct carries
631
+ # indent_text; returns an OWNED string — read via
632
+ # read_owned_string (the seam frees it).
633
+ attach_function :leptris_document_serialize_ext,
634
+ [:leptris_document, :pointer, :pointer], :pointer
614
635
  attach_function :leptris_document_serialize_into,
615
636
  [:leptris_document, :pointer, :size_t, :pointer, :pointer], :size_t
616
637
  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
@@ -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
@@ -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.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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