leptris 1.7.0 → 1.9.0

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: c730c4f07fa9f337b543dc841c39e3c18b36e9cc819cb94073153bfe183ed116
4
- data.tar.gz: 3536b7dd7fe1cae01ffb0c11e5f56f19bdfbba7acd9f07c3058185fa8bcb9642
3
+ metadata.gz: 899c376385f93633ec89fc5ecf3f6425a443d7093cb143d254d951e830546fa0
4
+ data.tar.gz: 8b3754c9c75104813795af56d3fe0c34788419442c13011df23debc1bc36ceef
5
5
  SHA512:
6
- metadata.gz: ac1842632da49256d913a14f9d7c7ea73b2753d5eafc15c33b3898d48e2238e0e24ca24096e454e17f158c1d795280f220612e3b70a2b32ceeccaa6b41c873b9
7
- data.tar.gz: 031ee5fa00251dbde3f1aad9c6dad15e5ef79c309de0d5bd4875028a1a1d2160b6a9fe4a53e698c24b8333b9214ad824523c9de89e598ede14cf8e6737030378
6
+ metadata.gz: 076ec92edb03051dcaf5b89b1104d1f39f9ca787fd4d7f3f72ff21a3fa9eecfc44fca6057c574996abeca94df51218bb8c1823e72ff8a924b3129ed14a88831a
7
+ data.tar.gz: 98db2fedbf8df3a09b6d44ff23f3661e37f069ba84bce091605adb394449ec48c8030e045203c8eb45980850442eb23e265e7e0b704b809ea30a75df3a5ce86d
data/CHANGELOG.md CHANGED
@@ -2,9 +2,56 @@
2
2
 
3
3
  All notable changes to Leptris will be documented in this file.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
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.0] - 2026-08-24
9
+
10
+ Lockstep with libleptris 1.9.0 (covers the 1.8.0 and 1.9.0 engine
11
+ releases; CI/build pin libleptris v1.9.0).
12
+
13
+ ### Added
14
+
15
+ - **Expanded-name attribute access** (engine #542):
16
+ `Element#attribute_ns(uri, local)` and `#has_attribute_ns?(uri,
17
+ local)` look attributes up by namespace URI + local name with XML
18
+ Namespaces 1.0 semantics — cross-prefix matches, nil/"" URI for
19
+ no-namespace only, xmlns declarations invisible. `Element#[]`
20
+ inherits the corrected by-name semantics: bare names match only
21
+ no-namespace attributes, qualified names resolve through
22
+ declarations, undeclared prefixes return nil.
23
+ - **Per-attribute namespace accessors** (engine #542): `Attr#prefix`
24
+ (as written in the QName) and `Attr#namespace_uri` (resolved
25
+ through the owning element's in-scope declarations at read time;
26
+ xml prebound; nil for undeclared prefixes). `Attr#namespace`
27
+ serves the URI instead of the pre-1.8.0 nil stub.
28
+ - **Recover parsing** (engine #547): `Leptris::XML.parse(xml,
29
+ recover: true)` / `ParseOptions.recovering` — a parse failure
30
+ returns an empty document (failure detail on the thread-global
31
+ last error) instead of raising ParseError: the libxml2
32
+ XML_PARSE_RECOVER semantics adapters emulate. Struct-only option;
33
+ carrying it routes the parse through `leptris_parse_string_ex`.
34
+ - **Caller-buffer serialization**: `#to_xml` (document + element)
35
+ now rides `leptris_document/element_serialize_into` (engine #541)
36
+ — options carried, the size-query + fill pair reuses one
37
+ serialization through a per-document cache invalidated on
38
+ mutation, and no C-side result-string allocation remains.
39
+
40
+ ### Fixed
41
+
42
+ - Rootless documents with document-level PIs no longer serialize to
43
+ "" (engine #546) — the declaration and every document-level PI are
44
+ emitted.
45
+ - Detached sibling inserts chain (engine #540): bottom-up
46
+ construction via `add_next_sibling` on detached elements works;
47
+ attaching the head of a detached chain carries the whole chain.
48
+ - Eager library resolution at require time (issue leptris-ruby#49):
49
+ the FFI library list is loaded when the gem loads, so a
50
+ ruby-platform install (no vendored libleptris) fails immediately
51
+ with an actionable message instead of deep inside Document.parse
52
+ - Packaging policy: this gem never publishes ruby-platform releases
53
+ without the vendored library — platform variants only
54
+
8
55
  ## [1.7.0] - 2026-08-24
9
56
 
10
57
  Lockstep with libleptris 1.7.0. New engine surface:
@@ -54,59 +101,6 @@ detached PI/comment/CDATA mutation works on rootless documents
54
101
  (#519); union nodesets keep attribute identity (#514); document-level
55
102
  processing instructions gained a public API (#526).
56
103
 
57
- ## [1.5.0] - 2026-08-24
58
-
59
- Lockstep with libleptris 1.5.0 — the engine pins the TODO.engine
60
- release: file-backed pull/iterparse streaming (bounded memory off
61
- disk), compiled XPath ns/vars contexts, Rust crate publish workflow.
62
-
63
- ## [1.4.0] - 2026-08-23
64
-
65
- Lockstep with libleptris 1.4.0 — engine pins the TODO.bindings
66
- release: pull (StAX) API, bounded iterparse, compiled XPath
67
- expressions, per-parse options, truthful serialization encoding
68
- declarations. Binding-side adoption of the new APIs follows.
69
-
70
- ## [1.6.2] - 2026-08-24
71
-
72
- Lockstep with libleptris 1.6.2 (fix releases, no public API changes).
73
-
74
- ### Changed
75
-
76
- - CI/build pin libleptris v1.6.2, carrying the serializer fix for
77
- mixed-content indentation (upstream #534 — never indent inside
78
- mixed-content elements).
79
-
80
- ## [1.6.1] - 2026-08-24
81
-
82
- ### Added
83
-
84
- - **Readonly mode**: `Leptris::XML.parse(xml, readonly: true)` /
85
- `Document#readonly!` (one-way). Mutations raise
86
- `Leptris::XML::ReadOnlyError`; read paths memoize aggressively
87
- (names, content, children NodeSets, attribute hashes) since they can
88
- never go stale; the C document is frozen (advisory upstream).
89
- Detached factories (`create_element` etc.) remain usable.
90
- Purpose: steady-state read performance for the dominant
91
- parse-query-serialize workload — see Changed.
92
-
93
- ### Changed
94
-
95
- - Micro-optimizations targeting the small-document gap versus C
96
- extension bindings:
97
- - node type is memoized from `Node.wrap`'s dispatch call — every
98
- predicate and `#type` is now FFI-free
99
- - the default serialize options struct is built once and reused
100
- - `Document.parse` skips the per-parse status MemoryPointer (the C
101
- out-param is nullable; failure detail comes from the thread-local
102
- last error)
103
- - `Element#name` memoizes (invalidated by `name=`)
104
-
105
- ## [1.6.0] - 2026-08-24
106
-
107
- Lockstep with libleptris 1.6.0 (v1.4/v1.5 were fix releases with no
108
- public API changes).
109
-
110
104
  ### Added
111
105
 
112
106
  - **`Leptris::XML::XPath` — compiled expressions** (parse once,
@@ -130,6 +124,19 @@ public API changes).
130
124
  - `leptris_parse_string_ex` bound (options-struct parse; the flags
131
125
  path remains the default).
132
126
 
127
+ ## [1.5.0] - 2026-08-24
128
+
129
+ Lockstep with libleptris 1.5.0 — the engine pins the TODO.engine
130
+ release: file-backed pull/iterparse streaming (bounded memory off
131
+ disk), compiled XPath ns/vars contexts, Rust crate publish workflow.
132
+
133
+ ## [1.4.0] - 2026-08-23
134
+
135
+ Lockstep with libleptris 1.4.0 — engine pins the TODO.bindings
136
+ release: pull (StAX) API, bounded iterparse, compiled XPath
137
+ expressions, per-parse options, truthful serialization encoding
138
+ declarations. Binding-side adoption of the new APIs follows.
139
+
133
140
  ## [1.3.0] - 2026-08-23
134
141
 
135
142
  Lockstep with libleptris 1.3.0 — takes full advantage of the
data/README.adoc CHANGED
@@ -91,6 +91,18 @@ doc = Leptris::XML.parse_file("books.xml")
91
91
  This is the direct Nokogiri equivalent of `Nokogiri::XML(...)`. The
92
92
  returned object is a `Leptris::XML::Document`.
93
93
 
94
+ Malformed input raises `Leptris::XML::ParseError`. Parse with
95
+ `recover: true` to get libxml2's `XML_PARSE_RECOVER` semantics
96
+ instead — an empty document back, with the failure recorded on the
97
+ thread-global last error:
98
+
99
+ [source,ruby]
100
+ ----
101
+ doc = Leptris::XML.parse("<broken", recover: true)
102
+ doc.root # => nil
103
+ Leptris::XML::FFI.leptris_last_error # => "..."
104
+ ----
105
+
94
106
  == Reading nodes
95
107
 
96
108
  [horizontal]
@@ -251,6 +263,10 @@ doc.root.name = "catalog"
251
263
  `Element#add_namespace_definition(prefix, href)` (alias `#add_namespace`) :: declare `xmlns:prefix="href"` on this element.
252
264
  `Element#default_namespace=(href)` :: declare/replace `xmlns="href"`.
253
265
  `Element#remove_namespace_definition(prefix)` :: drop a declaration.
266
+ `Element#attribute_ns(uri, local)` :: attribute value by expanded name (URI + local); nil URI matches no-namespace attributes only.
267
+ `Element#has_attribute_ns?(uri, local)` :: presence by expanded name.
268
+ `Attr#prefix` :: the attribute's prefix as written (`nil` when none).
269
+ `Attr#namespace_uri` :: resolved through the owning element's in-scope declarations at read time (`xml` prebound; `nil` for undeclared prefixes).
254
270
 
255
271
  [source,ruby]
256
272
  ----
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.7.0"
11
+ LIBLEPTRIS_VERSION = "1.9.0"
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.7.0"
4
+ VERSION = "1.9.0"
5
5
  end
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Lightweight attribute wrapper: a (name, value, parent_element) triple
4
- # materialized from the attribute-iteration face. Mutation goes through
5
- # the parent element via #[].
3
+ # Lightweight attribute wrapper: a (name, value, parent_element)
4
+ # triple materialized from the attribute-iteration face, carrying
5
+ # the C attribute handle for the per-attribute namespace accessors.
6
+ # Mutation goes through the parent element via #[].
6
7
 
7
8
  class Leptris::XML::Attr
8
9
  attr_reader :name, :value, :element
9
10
 
10
- def initialize(name, value, element)
11
+ # c_handle is the C LeptrisAttribute pointer from the owning
12
+ # element's iteration face — document-owned and borrowed. Absent
13
+ # only when an Attr is constructed by hand.
14
+ def initialize(name, value, element, c_handle: nil)
11
15
  @name = name
12
16
  @value = value
13
17
  @element = element
18
+ @c_handle = c_handle
14
19
  end
15
20
 
16
21
  def value=(new_value)
@@ -18,8 +23,25 @@ class Leptris::XML::Attr
18
23
  @value = new_value
19
24
  end
20
25
 
26
+ # The prefix as written in the qualified name (bytes before the
27
+ # colon), nil for a no-namespace attribute. Name-derived and
28
+ # immutable — the same semantics as leptris_attribute_prefix, so
29
+ # no declaration lookup is involved.
30
+ def prefix
31
+ @name.include?(":") ? @name.split(":", 2).first : nil
32
+ end
33
+
34
+ # The attribute's namespace URI, resolved through the OWNING
35
+ # element's in-scope declarations at read time (libleptris 1.8.0).
36
+ # nil for a no-namespace attribute or an undeclared prefix; xml
37
+ # is prebound to http://www.w3.org/XML/1998/namespace.
38
+ def namespace_uri
39
+ return nil if @c_handle.nil?
40
+ Leptris::XML::FFI.leptris_attribute_namespace_uri(@c_handle)
41
+ end
42
+
21
43
  def namespace
22
- nil # upstream gap: per-attribute namespace not exposed
44
+ namespace_uri
23
45
  end
24
46
 
25
47
  def remove
@@ -34,23 +34,41 @@ class Leptris::XML::Document
34
34
  @wrapper_cache = {}
35
35
  end
36
36
 
37
- def self.parse(xml_or_io, options: nil, readonly: false)
37
+ def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
38
38
  xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
39
39
  if xml.empty?
40
40
  raise Leptris::XML::ParseError, "empty input"
41
41
  end
42
- flags = resolve_flags(options)
42
+ if options.nil?
43
+ options = Leptris::XML::ParseOptions.new(recover: recover)
44
+ elsif recover && !options.recover?
45
+ options = options | Leptris::XML::ParseOptions.new(recover: true)
46
+ elsif !options.is_a?(Leptris::XML::ParseOptions)
47
+ raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
48
+ end
43
49
  # The status out-param is nullable; the thread-local last error
44
50
  # carries failure detail, and skipping the per-parse MemoryPointer
45
51
  # is measurable on small documents.
46
52
  raw =
47
- if flags.zero?
53
+ if options.struct_required?
54
+ # Recover is a struct field, not a parse flag — the options
55
+ # struct path (leptris_parse_string_ex) is the only carrier.
56
+ options_struct = options.to_c_struct
57
+ Leptris::XML::FFI.leptris_parse_string_ex(
58
+ xml, xml.bytesize, options_struct.pointer, nil)
59
+ elsif options.flags.zero?
48
60
  Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
49
61
  else
50
62
  Leptris::XML::FFI.leptris_parse_string_flags(
51
- xml, xml.bytesize, flags, nil)
63
+ xml, xml.bytesize, options.flags, nil)
52
64
  end
53
65
  if raw.null?
66
+ if options.recover?
67
+ # Unreachable in practice: recover returns an empty document
68
+ # rather than NULL; kept so a contract change fails loudly.
69
+ raise Leptris::XML::Error,
70
+ "leptris_parse_string_ex returned NULL under recover"
71
+ end
54
72
  raise Leptris::XML::ParseError,
55
73
  "leptris_parse_string failed: " +
56
74
  Leptris::XML::FFI.leptris_last_error.to_s
@@ -102,15 +120,6 @@ class Leptris::XML::Document
102
120
  end
103
121
  private_class_method :finalizer
104
122
 
105
- def self.resolve_flags(options)
106
- return Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT if options.nil?
107
- unless options.is_a?(Leptris::XML::ParseOptions)
108
- raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
109
- end
110
- options.flags
111
- end
112
- private_class_method :resolve_flags
113
-
114
123
  def root
115
124
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
116
125
  return nil if @c_ptr.nil?
@@ -182,7 +191,7 @@ class Leptris::XML::Document
182
191
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
183
192
  return "" if @c_ptr.nil?
184
193
  Leptris::XML::Serialization.to_xml(
185
- Leptris::XML::FFI.method(:leptris_document_serialize), @c_ptr,
194
+ Leptris::XML::FFI.method(:leptris_document_serialize_into), @c_ptr,
186
195
  indent: indent, no_decl: no_decl, encoding: encoding)
187
196
  end
188
197
  alias_method :to_s, :to_xml
@@ -47,6 +47,21 @@ class Leptris::XML::Element < Leptris::XML::Node
47
47
  end
48
48
  alias_method :has_attribute?, :key?
49
49
 
50
+ # Expanded-name attribute access (libleptris 1.8.0): URI + local
51
+ # name with XML Namespaces 1.0 semantics — the written prefix
52
+ # never matters, only what it resolves to through this element's
53
+ # in-scope declarations. uri nil/"" matches no-namespace
54
+ # attributes only; xmlns declarations are invisible.
55
+ def attribute_ns(uri, local)
56
+ Leptris::XML::FFI.leptris_element_attribute_ns(
57
+ @c_ptr, uri&.to_s, local.to_s)
58
+ end
59
+
60
+ def has_attribute_ns?(uri, local)
61
+ Leptris::XML::FFI.leptris_element_has_attribute_ns(
62
+ @c_ptr, uri&.to_s, local.to_s) != 0
63
+ end
64
+
50
65
  def remove_attribute(name)
51
66
  ensure_writable!
52
67
  Leptris::XML::FFI.check_status(
@@ -58,13 +73,15 @@ class Leptris::XML::Element < Leptris::XML::Node
58
73
  # Iterates the element's attributes via the v1.1.0 linked-list face
59
74
  # (one FFI call per attribute; the name_at/value_at indexing API
60
75
  # re-walks the list per index, making it O(n^2) per element).
76
+ # The C attribute handle rides along so Attr can serve the
77
+ # per-attribute namespace accessors (libleptris 1.8.0).
61
78
  def each_attribute
62
79
  return enum_for(:each_attribute) unless block_given?
63
80
  attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
64
81
  until attr.nil? || attr.null?
65
82
  name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
66
83
  value = Leptris::XML::FFI.leptris_attribute_get_value(@c_ptr, attr)
67
- yield Leptris::XML::Attr.new(name, value, self)
84
+ yield Leptris::XML::Attr.new(name, value, self, c_handle: attr)
68
85
  attr = Leptris::XML::FFI.leptris_attribute_next(attr)
69
86
  end
70
87
  self
@@ -238,7 +255,7 @@ class Leptris::XML::Element < Leptris::XML::Node
238
255
 
239
256
  def to_xml(indent: 0, no_decl: false, encoding: nil)
240
257
  Leptris::XML::Serialization.to_xml(
241
- Leptris::XML::FFI.method(:leptris_element_serialize), @c_ptr,
258
+ Leptris::XML::FFI.method(:leptris_element_serialize_into), @c_ptr,
242
259
  indent: indent, no_decl: no_decl, encoding: encoding)
243
260
  end
244
261
 
@@ -7,15 +7,32 @@ module Leptris
7
7
  module FFI
8
8
  extend ::FFI::Library
9
9
 
10
- ffi_lib [
11
- ENV["LEPTRIS_LIB_PATH"],
12
- File.expand_path("../../libleptris.dylib", __dir__),
13
- File.expand_path("../../libleptris.so", __dir__),
14
- File.expand_path("../../libleptris.dll", __dir__),
15
- "/usr/local/lib/libleptris.dylib",
16
- "/usr/local/lib/libleptris.so",
17
- "leptris",
18
- ].compact
10
+ # Issue leptris-ruby#49: name the remedy when the library is
11
+ # missing (ruby-platform gem without a vendored libleptris).
12
+ begin
13
+ ffi_lib [
14
+ ENV["LEPTRIS_LIB_PATH"],
15
+ File.expand_path("../../libleptris.dylib", __dir__),
16
+ File.expand_path("../../libleptris.so", __dir__),
17
+ File.expand_path("../../libleptris.dll", __dir__),
18
+ "/usr/local/lib/libleptris.dylib",
19
+ "/usr/local/lib/libleptris.so",
20
+ "leptris",
21
+ ].compact
22
+ rescue LoadError => e
23
+ raise LoadError, <<~MSG
24
+ leptris: cannot load the vendored libleptris library.
25
+ You may have installed the ruby-platform variant of this
26
+ gem (#{Gem.loaded_specs["leptris"]&.full_name rescue "unknown"}),
27
+ which does not vendor the native library. Fix:
28
+
29
+ bundle update leptris # force platform gems
30
+ # or: gem install leptris --platform ruby is unsupported
31
+ # or: set LEPTRIS_LIB_PATH=/path/to/libleptris.so
32
+
33
+ Original error: #{e.message}
34
+ MSG
35
+ end
19
36
 
20
37
  typedef :pointer, :leptris_document
21
38
  typedef :pointer, :leptris_element
@@ -53,6 +70,16 @@ module Leptris
53
70
  :encoding, :pointer
54
71
  end
55
72
 
73
+ # Mirrors LeptrisParseOptions (libleptris >= 1.9.0 carries the
74
+ # recover field; earlier fields unchanged since v1.6.0).
75
+ class ParseOptionsStruct < ::FFI::Struct
76
+ layout \
77
+ :flags, :int,
78
+ :strict_mode, :int,
79
+ :max_depth, :int,
80
+ :recover, :int
81
+ end
82
+
56
83
  attach_function :leptris_version, [], :string
57
84
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
58
85
 
@@ -209,6 +236,20 @@ module Leptris
209
236
  [:leptris_element, :string, :int], :int
210
237
  attach_function :leptris_element_has_attribute,
211
238
  [:leptris_element, :string], :int
239
+ # libleptris 1.8.0: expanded-name lookup — (uri, local) with
240
+ # XML Namespaces 1.0 semantics; NULL/"" uri matches only
241
+ # no-namespace attributes, xmlns declarations never match.
242
+ attach_function :leptris_element_attribute_ns,
243
+ [:leptris_element, :string, :string], :string
244
+ attach_function :leptris_element_has_attribute_ns,
245
+ [:leptris_element, :string, :string], :int
246
+ # libleptris 1.8.0: per-attribute prefix (name-derived) and
247
+ # namespace URI (resolved through the owning element's
248
+ # in-scope declarations at read time).
249
+ attach_function :leptris_attribute_prefix,
250
+ [:leptris_attribute], :string
251
+ attach_function :leptris_attribute_namespace_uri,
252
+ [:leptris_attribute], :string
212
253
  attach_function :leptris_element_first_attribute,
213
254
  [:leptris_element], :leptris_attribute
214
255
  attach_function :leptris_attribute_next,
@@ -451,10 +492,18 @@ module Leptris
451
492
 
452
493
  attach_function :leptris_document_serialize,
453
494
  [:leptris_document, :pointer], :pointer
495
+ # libleptris >= 1.9.0: caller-buffer serialization with options
496
+ # (leptris#541). buf=NULL is a size query; the size-query +
497
+ # fill pair reuses one serialization through a per-document
498
+ # cache invalidated on mutation.
499
+ attach_function :leptris_document_serialize_into,
500
+ [:leptris_document, :pointer, :size_t, :pointer, :pointer], :size_t
454
501
  attach_function :leptris_document_get_dtd,
455
502
  [:leptris_document], :pointer
456
503
  attach_function :leptris_element_serialize,
457
504
  [:leptris_element, :pointer], :pointer
505
+ attach_function :leptris_element_serialize_into,
506
+ [:leptris_element, :pointer, :size_t, :pointer, :pointer], :size_t
458
507
  attach_function :leptris_document_save_file,
459
508
  [:leptris_document, :string, :pointer], :leptris_status
460
509
  attach_function :leptris_c14n_canonicalize,
@@ -11,19 +11,52 @@ class Leptris::XML::ParseOptions
11
11
 
12
12
  attr_reader :flags
13
13
 
14
- def initialize(flags = Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT)
14
+ # Recover (libleptris 1.9.0, #547): a parse failure returns an
15
+ # empty document with the failure recorded in Document#last_error
16
+ # instead of raising — the libxml2 XML_PARSE_RECOVER semantics
17
+ # adapters emulate. Struct-only (not a parse flag): carrying it
18
+ # routes Document.parse through leptris_parse_string_ex.
19
+ attr_reader :recover
20
+
21
+ def initialize(flags = Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT,
22
+ recover: false)
15
23
  @flags = flags.to_i
24
+ @recover = recover ? true : false
16
25
  end
17
26
 
18
27
  def self.noblanks
19
28
  new(NOBLANKS)
20
29
  end
21
30
 
31
+ def self.recovering
32
+ new(recover: true)
33
+ end
34
+
22
35
  def noblanks?
23
36
  @flags & NOBLANKS != 0
24
37
  end
25
38
 
39
+ def recover?
40
+ @recover == true
41
+ end
42
+
26
43
  def |(other)
27
- self.class.new(@flags | other.flags)
44
+ self.class.new(@flags | other.flags, recover: @recover || other.recover?)
45
+ end
46
+
47
+ # True when the options cannot ride the flags-only parse path and
48
+ # need the full LeptrisParseOptions struct (leptris_parse_string_ex).
49
+ def struct_required?
50
+ recover?
51
+ end
52
+
53
+ # Builds the C LeptrisParseOptions struct mirroring this instance.
54
+ def to_c_struct
55
+ struct = Leptris::XML::FFI::ParseOptionsStruct.new
56
+ struct[:flags] = @flags
57
+ struct[:strict_mode] = -1
58
+ struct[:max_depth] = 0
59
+ struct[:recover] = @recover ? 1 : 0
60
+ struct
28
61
  end
29
62
  end
@@ -18,8 +18,12 @@ module Leptris::XML::Serialization
18
18
  end
19
19
  private_constant :DEFAULT_OPTIONS
20
20
 
21
- # +ffi_function+ is a bound FFI function taking (c_ptr, opts_ptr):
22
- # leptris_document_serialize or leptris_element_serialize.
21
+ # +ffi_function+ is a bound _serialize_into function taking
22
+ # (c_ptr, buf, capacity, out_len, opts): size query with buf=NULL,
23
+ # then one fill into a caller buffer. Since libleptris 1.9.0 the
24
+ # pair reuses a single serialization through a per-document cache
25
+ # (leptris#541) — no C-side result-string allocation, and repeated
26
+ # serializations of an unmutated document skip the serialize pass.
23
27
  def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
24
28
  opts =
25
29
  if indent.to_i.zero? && !no_decl && encoding.nil?
@@ -29,8 +33,18 @@ module Leptris::XML::Serialization
29
33
  indent: indent, no_decl: no_decl, encoding: encoding)
30
34
  opts
31
35
  end
32
- str_ptr = ffi_function.call(c_ptr, opts.pointer)
33
- Leptris::XML::FFI.read_owned_string(str_ptr)
36
+ need = ffi_function.call(c_ptr, nil, 0, nil, opts.pointer)
37
+ return "" if need.zero?
38
+ buf = ::FFI::MemoryPointer.new(:char, need)
39
+ begin
40
+ ffi_function.call(c_ptr, buf, need, nil, opts.pointer)
41
+ # The buffer holds exactly the serialization + NUL; XML output
42
+ # is NUL-free, so a bounded read_string is exact. (Avoids the
43
+ # out_len param — no portable size_t read exists on Pointer.)
44
+ buf.read_string
45
+ ensure
46
+ buf.free
47
+ end
34
48
  end
35
49
 
36
50
  # +ffi_function+ is a bound FFI function taking
data/lib/leptris/xml.rb CHANGED
@@ -27,8 +27,9 @@ module Leptris
27
27
 
28
28
  # Nokogiri-style top-level entry points (Nokogiri::XML(...) /
29
29
  # Nokogiri::XML.parse). Delegates to Document.parse.
30
- def self.parse(xml_or_io, options: nil, readonly: false)
31
- Document.parse(xml_or_io, options: options, readonly: readonly)
30
+ def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
31
+ Document.parse(xml_or_io, options: options, readonly: readonly,
32
+ recover: recover)
32
33
  end
33
34
 
34
35
  def self.parse_file(path)
data/lib/leptris.rb CHANGED
@@ -3,5 +3,12 @@
3
3
  require "leptris/version"
4
4
 
5
5
  module Leptris
6
+ # Eager library resolution (issue leptris-ruby#49): autoload made
7
+ # ffi_lib lazy, so a ruby-platform gem (no vendored libleptris)
8
+ # failed deep inside Document.parse instead of at require time.
9
+ # Loading the FFI module NOW turns that into an immediate,
10
+ # actionable LoadError.
11
+ require "leptris/xml/ffi"
12
+
6
13
  autoload :XML, "leptris/xml"
7
14
  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.7.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.