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 +4 -4
- data/CHANGELOG.md +61 -54
- data/README.adoc +16 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/attr.rb +27 -5
- data/lib/leptris/xml/document.rb +23 -14
- data/lib/leptris/xml/element.rb +19 -2
- data/lib/leptris/xml/ffi.rb +58 -9
- data/lib/leptris/xml/parse_options.rb +35 -2
- data/lib/leptris/xml/serialization.rb +18 -4
- data/lib/leptris/xml.rb +3 -2
- data/lib/leptris.rb +7 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 899c376385f93633ec89fc5ecf3f6425a443d7093cb143d254d951e830546fa0
|
|
4
|
+
data.tar.gz: 8b3754c9c75104813795af56d3fe0c34788419442c13011df23debc1bc36ceef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.0"
|
|
12
12
|
|
|
13
13
|
CMAKE_FLAGS = %w[
|
|
14
14
|
-DCMAKE_BUILD_TYPE=Release
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/attr.rb
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Lightweight attribute wrapper: a (name, value, parent_element)
|
|
4
|
-
# materialized from the attribute-iteration face
|
|
5
|
-
# the
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
namespace_uri
|
|
23
45
|
end
|
|
24
46
|
|
|
25
47
|
def remove
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(:
|
|
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
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -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(:
|
|
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
|
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -7,15 +7,32 @@ module Leptris
|
|
|
7
7
|
module FFI
|
|
8
8
|
extend ::FFI::Library
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
#
|
|
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
|
-
|
|
33
|
-
|
|
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
|