leptris 1.7.0 → 1.9.1

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: ed2f1d5755923f389734df36cc9c40c513535e58e7abe5a40aa87dd93f858788
4
+ data.tar.gz: eb4e9c9c3b12a7584572746053c9aaf318ee5481fa002909d8502d5565bcf543
5
5
  SHA512:
6
- metadata.gz: ac1842632da49256d913a14f9d7c7ea73b2753d5eafc15c33b3898d48e2238e0e24ca24096e454e17f158c1d795280f220612e3b70a2b32ceeccaa6b41c873b9
7
- data.tar.gz: 031ee5fa00251dbde3f1aad9c6dad15e5ef79c309de0d5bd4875028a1a1d2160b6a9fe4a53e698c24b8333b9214ad824523c9de89e598ede14cf8e6737030378
6
+ metadata.gz: 35a1a34584014746679c39098f60c6009871007e3e5016562b669437e6f00d84412c7a26f286034f628421f72846c810b1b3568faacbbae86fa3e6c47b6db20f
7
+ data.tar.gz: 5e4785ee975db46c2f7798a4effd7c3d2be729e94ded98db78e3cdd830891b0704170e104d9b2287ff1e4e8a98833917a70a7ec4d5591615931020cf67f18e54
data/CHANGELOG.md CHANGED
@@ -2,70 +2,78 @@
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.7.0] - 2026-08-24
8
+ ## [1.9.1] - 2026-08-24
9
9
 
10
- Lockstep with libleptris 1.7.0. New engine surface:
11
- leptris_node_children (all-kind batch) and the document/element
12
- serialize-into caller buffers (#535).
10
+ ### Fixed
13
11
 
14
- ## [1.6.2] - 2026-08-24
12
+ - **Autoload manifest restored after `require "leptris"`** (issue
13
+ #53): 1.9.0's eager FFI require ran inside `module Leptris`
14
+ before `autoload :XML` was registered, so ffi.rb's module opening
15
+ created `Leptris::XML` first — the autoload registration was
16
+ shadowed, xml.rb never loaded, and the entire API was unreachable
17
+ (`Leptris::XML.constants == [:FFI]`, no `Leptris::XML.parse`).
18
+ The eager require now runs after the registration: ffi.rb's
19
+ module opening triggers the autoload, FFI lands inside the real
20
+ manifest module, and a downstream `require "leptris/xml"` is a
21
+ no-op (the moxml workaround becomes unnecessary). Eager library
22
+ resolution at require time (#49) is unchanged. Regression spec
23
+ runs the cold `require "leptris"` path in a subprocess.
15
24
 
16
- Lockstep with libleptris 1.6.2 (fix releases, no public API changes).
25
+ ## [1.9.0] - 2026-08-24
17
26
 
18
- ### Changed
19
-
20
- - CI/build pin libleptris v1.6.2, carrying the serializer fix for
21
- mixed-content indentation (upstream #534 — never indent inside
22
- mixed-content elements).
23
-
24
- ## [1.6.1] - 2026-08-24
27
+ Lockstep with libleptris 1.9.0 (covers the 1.8.0 and 1.9.0 engine
28
+ releases; CI/build pin libleptris v1.9.0).
25
29
 
26
30
  ### Added
27
31
 
28
- - **Readonly mode**: `Leptris::XML.parse(xml, readonly: true)` /
29
- `Document#readonly!` (one-way). Mutations raise
30
- `Leptris::XML::ReadOnlyError`; read paths memoize aggressively
31
- (names, content, children NodeSets, attribute hashes) since they can
32
- never go stale; the C document is frozen (advisory upstream).
33
- Detached factories (`create_element` etc.) remain usable.
34
- Purpose: steady-state read performance for the dominant
35
- parse-query-serialize workload see Changed.
36
-
37
- ### Changed
38
-
39
- - Micro-optimizations targeting the small-document gap versus C
40
- extension bindings:
41
- - node type is memoized from `Node.wrap`'s dispatch call — every
42
- predicate and `#type` is now FFI-free
43
- - the default serialize options struct is built once and reused
44
- - `Document.parse` skips the per-parse status MemoryPointer (the C
45
- out-param is nullable; failure detail comes from the thread-local
46
- last error)
47
- - `Element#name` memoizes (invalidated by `name=`)
48
-
49
- ## [1.6.0] - 2026-08-24
50
-
51
- Lockstep with libleptris 1.6.0 — the moxml-adapter blockers fixed:
52
- same-parent node moves no longer corrupt the sibling chain (#518),
53
- detached PI/comment/CDATA mutation works on rootless documents
54
- (#519); union nodesets keep attribute identity (#514); document-level
55
- processing instructions gained a public API (#526).
32
+ - **Expanded-name attribute access** (engine #542):
33
+ `Element#attribute_ns(uri, local)` and `#has_attribute_ns?(uri,
34
+ local)` look attributes up by namespace URI + local name with XML
35
+ Namespaces 1.0 semantics cross-prefix matches, nil/"" URI for
36
+ no-namespace only, xmlns declarations invisible. `Element#[]`
37
+ inherits the corrected by-name semantics: bare names match only
38
+ no-namespace attributes, qualified names resolve through
39
+ declarations, undeclared prefixes return nil.
40
+ - **Per-attribute namespace accessors** (engine #542): `Attr#prefix`
41
+ (as written in the QName) and `Attr#namespace_uri` (resolved
42
+ through the owning element's in-scope declarations at read time;
43
+ xml prebound; nil for undeclared prefixes). `Attr#namespace`
44
+ serves the URI instead of the pre-1.8.0 nil stub.
45
+ - **Recover parsing** (engine #547): `Leptris::XML.parse(xml,
46
+ recover: true)` / `ParseOptions.recovering` a parse failure
47
+ returns an empty document (failure detail on the thread-global
48
+ last error) instead of raising ParseError: the libxml2
49
+ XML_PARSE_RECOVER semantics adapters emulate. Struct-only option;
50
+ carrying it routes the parse through `leptris_parse_string_ex`.
51
+ - **Caller-buffer serialization**: `#to_xml` (document + element)
52
+ now rides `leptris_document/element_serialize_into` (engine #541)
53
+ options carried, the size-query + fill pair reuses one
54
+ serialization through a per-document cache invalidated on
55
+ mutation, and no C-side result-string allocation remains.
56
56
 
57
- ## [1.5.0] - 2026-08-24
57
+ ### Fixed
58
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.
59
+ - Rootless documents with document-level PIs no longer serialize to
60
+ "" (engine #546) the declaration and every document-level PI are
61
+ emitted.
62
+ - Detached sibling inserts chain (engine #540): bottom-up
63
+ construction via `add_next_sibling` on detached elements works;
64
+ attaching the head of a detached chain carries the whole chain.
65
+ - Eager library resolution at require time (issue leptris-ruby#49):
66
+ the FFI library list is loaded when the gem loads, so a
67
+ ruby-platform install (no vendored libleptris) fails immediately
68
+ with an actionable message instead of deep inside Document.parse
69
+ - Packaging policy: this gem never publishes ruby-platform releases
70
+ without the vendored library — platform variants only
62
71
 
63
- ## [1.4.0] - 2026-08-23
72
+ ## [1.7.0] - 2026-08-24
64
73
 
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.
74
+ Lockstep with libleptris 1.7.0. New engine surface:
75
+ leptris_node_children (all-kind batch) and the document/element
76
+ serialize-into caller buffers (#535).
69
77
 
70
78
  ## [1.6.2] - 2026-08-24
71
79
 
@@ -104,8 +112,11 @@ Lockstep with libleptris 1.6.2 (fix releases, no public API changes).
104
112
 
105
113
  ## [1.6.0] - 2026-08-24
106
114
 
107
- Lockstep with libleptris 1.6.0 (v1.4/v1.5 were fix releases with no
108
- public API changes).
115
+ Lockstep with libleptris 1.6.0 the moxml-adapter blockers fixed:
116
+ same-parent node moves no longer corrupt the sibling chain (#518),
117
+ detached PI/comment/CDATA mutation works on rootless documents
118
+ (#519); union nodesets keep attribute identity (#514); document-level
119
+ processing instructions gained a public API (#526).
109
120
 
110
121
  ### Added
111
122
 
@@ -130,6 +141,19 @@ public API changes).
130
141
  - `leptris_parse_string_ex` bound (options-struct parse; the flags
131
142
  path remains the default).
132
143
 
144
+ ## [1.5.0] - 2026-08-24
145
+
146
+ Lockstep with libleptris 1.5.0 — the engine pins the TODO.engine
147
+ release: file-backed pull/iterparse streaming (bounded memory off
148
+ disk), compiled XPath ns/vars contexts, Rust crate publish workflow.
149
+
150
+ ## [1.4.0] - 2026-08-23
151
+
152
+ Lockstep with libleptris 1.4.0 — engine pins the TODO.bindings
153
+ release: pull (StAX) API, bounded iterparse, compiled XPath
154
+ expressions, per-parse options, truthful serialization encoding
155
+ declarations. Binding-side adoption of the new APIs follows.
156
+
133
157
  ## [1.3.0] - 2026-08-23
134
158
 
135
159
  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.1"
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
@@ -5,3 +5,29 @@ require "leptris/version"
5
5
  module Leptris
6
6
  autoload :XML, "leptris/xml"
7
7
  end
8
+
9
+ # Eager library resolution (issue leptris-ruby#49): resolve the
10
+ # native library at require time instead of lazily inside
11
+ # Document.parse, so a missing library fails immediately.
12
+ #
13
+ # The require MUST come after the autoload registration above.
14
+ # ffi.rb opens `module Leptris; module XML`, and when that runs
15
+ # before `autoload :XML` exists it creates the constant first — the
16
+ # later autoload registration is then shadowed, xml.rb never loads,
17
+ # and the entire API is unreachable (Leptris::XML.constants ==
18
+ # [:FFI]; issue leptris-ruby#53). Requiring from here after the
19
+ # registration makes ffi.rb's module opening trigger the autoload,
20
+ # landing FFI inside the real manifest module; a downstream
21
+ # `require "leptris/xml"` is then a no-op.
22
+ begin
23
+ require "leptris/xml/ffi"
24
+ rescue LoadError => e
25
+ raise LoadError, <<~MSG
26
+ Leptris could not load the native libleptris library.
27
+ The ruby-platform gem is a fallback variant and ships no
28
+ binary; install the platform-specific variant for your system
29
+ (e.g. `gem install leptris --platform=arm64-darwin`), or set
30
+ LEPTRIS_LIB_PATH to a libleptris.{so,dylib,dll} file.
31
+ (Underlying error: #{e.message})
32
+ MSG
33
+ 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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.