leptris 1.0.0 → 1.1.2
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 +90 -0
- data/README.adoc +1 -1
- data/Rakefile +91 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/attr.rb +3 -5
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +2 -3
- data/lib/leptris/xml/comment.rb +2 -3
- data/lib/leptris/xml/document.rb +53 -43
- data/lib/leptris/xml/element.rb +63 -82
- data/lib/leptris/xml/ffi.rb +61 -6
- data/lib/leptris/xml/node.rb +5 -15
- data/lib/leptris/xml/node_set.rb +8 -1
- data/lib/leptris/xml/parse_options.rb +21 -11
- data/lib/leptris/xml/processing_instruction.rb +4 -6
- data/lib/leptris/xml/sax/parser.rb +3 -15
- data/lib/leptris/xml/searchable.rb +1 -2
- data/lib/leptris/xml/serialization.rb +41 -0
- data/lib/leptris/xml/text.rb +2 -3
- data/lib/leptris/xml.rb +4 -3
- metadata +8 -4
- data/lib/leptris/xml/c14n.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 949fe73711448bd7857072004c75d6201b06829e9eb632124ae922a2fb7ff3f1
|
|
4
|
+
data.tar.gz: 259bbac7ce3e90389cce4f0e79dc979e7bc8ca844caa7745680ba352c010ba86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdab285d1fe462fcf7bc66681ebfbe1af74748ae99a8f0eebe1e502dfd72313b4429ac5943a18ebebd5706a308b135c33dfd7e8aab4bd313e383adcd91bb682f
|
|
7
|
+
data.tar.gz: 18240f20ed093b9e6434d0f9036ab93545b6af47237aa5ee72f616f3b10836148a3850fec5332fb2e44f416a53d25523d69f8bc4e4e1bb17c1f1181f16515f98
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,96 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.1] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Precompiled platform gems** (emf2svg-ruby model): `rake compile`
|
|
13
|
+
vendors libleptris into `lib/`, and `rake gem:native:<platform>`
|
|
14
|
+
builds binary gems for x86_64/aarch64-linux (incl. musl, via Alpine
|
|
15
|
+
containers), x64-mingw32/ucrt + aarch64-mingw-ucrt, and
|
|
16
|
+
x86_64/arm64-darwin. `gem install leptris` resolves the platform
|
|
17
|
+
gem and works with no system libleptris and no env vars — the FFI
|
|
18
|
+
search order prefers the vendored library. The pure-Ruby gem
|
|
19
|
+
remains the fallback for other setups.
|
|
20
|
+
- `Element#prefix` — the element's own namespace prefix (nil-safe).
|
|
21
|
+
- `Element#each_attribute` — Enumerator over the v1.1.0
|
|
22
|
+
attribute-iteration face; `attributes`/`attribute_nodes`/`keys`/
|
|
23
|
+
`values` now use it (O(n) vs the O(n^2) index-re-walk API).
|
|
24
|
+
- Bindings for the v1.1.0 public surface: attribute-iteration face,
|
|
25
|
+
mixed-nodeset API (`xpath_result_node_kind/get_node/node_name/
|
|
26
|
+
node_value` + kind constants), `leptris_serialize_document`,
|
|
27
|
+
`leptris_document_get_dtd`, `leptris_element_prefix`,
|
|
28
|
+
`leptris_xpath_register_function` (Ruby sugar deferred until the
|
|
29
|
+
callback return-string ownership is verified).
|
|
30
|
+
- `NodeSet` fetches via `get_node` (all node kinds) instead of the
|
|
31
|
+
elements-only `result_get`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `release.yml` is standalone again, building the 8-platform matrix
|
|
36
|
+
plus musl containers and publishing all gems via OIDC trusted
|
|
37
|
+
publishing. Requires the rubygems.org trusted publisher to be
|
|
38
|
+
re-pointed at this workflow (filename `release.yml`, no
|
|
39
|
+
reusable-workflow fields).
|
|
40
|
+
- CI builds libleptris via `rake compile` (single version pin in the
|
|
41
|
+
Rakefile) instead of an inline cmake script.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Removes the resurrected `c14n.rb` (its deletion was lost in a
|
|
46
|
+
rebase-merge; nothing referenced it).
|
|
47
|
+
|
|
48
|
+
### Known issues
|
|
49
|
+
|
|
50
|
+
- Mixed-kind XPath results misreport node types upstream
|
|
51
|
+
(leptris#477: enum-space collision; `node_name`/`node_value`
|
|
52
|
+
crash on text entries). Specs assert only the correct surface;
|
|
53
|
+
`NodeSet` carries a per-index fallback for the under-copying batch
|
|
54
|
+
accessor.
|
|
55
|
+
|
|
56
|
+
## [1.1.0] - 2026-08-22
|
|
57
|
+
|
|
58
|
+
Lockstep with libleptris 1.1.0.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `Leptris::XML.parse(xml, options:)` and
|
|
63
|
+
`Leptris::XML::ParseOptions.noblanks` — whitespace-only text nodes
|
|
64
|
+
can now be dropped at parse time (libxml2 `XML_PARSE_NOBLANKS` /
|
|
65
|
+
Nokogiri `noblanks` parity), via the new
|
|
66
|
+
`leptris_parse_string_flags` FFI entry point.
|
|
67
|
+
- Wrapper identity is now a guarantee: every path to a C node
|
|
68
|
+
(`root`, factories, navigation, xpath results) goes through
|
|
69
|
+
`Node.wrap` and its per-document cache, so the same node always
|
|
70
|
+
yields the same Ruby object.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- Architecture deepening: one status seam (`FFI.check_status`)
|
|
75
|
+
replaces 21 copy-pasted raises; one owned-string read
|
|
76
|
+
(`FFI.read_owned_string`) replaces 6 hand-rolled read-then-free
|
|
77
|
+
dances; a `Serialization` module owns the serialize/canonicalize
|
|
78
|
+
options lifecycle for both `Document` and `Element`; a
|
|
79
|
+
`CStringArray` adapter owns the NULL-terminated `char**` wire
|
|
80
|
+
format in both directions (c14n.rb folded in; the eager require is
|
|
81
|
+
gone and the autoload convention is uniform again).
|
|
82
|
+
|
|
83
|
+
### Removed
|
|
84
|
+
|
|
85
|
+
- `ParseOptions` constants that mapped to nothing (`RECOVER`,
|
|
86
|
+
`STRICT`, `NOCDATA`, …). They were never honored by any code path;
|
|
87
|
+
the class now exposes only the real flag surface.
|
|
88
|
+
|
|
89
|
+
### Changed (libleptris 1.1.0 lockstep)
|
|
90
|
+
|
|
91
|
+
- `Element#key?` now calls native `leptris_element_has_attribute`
|
|
92
|
+
(the v0.4.4-era export gap is closed) instead of emulating via
|
|
93
|
+
attribute lookup. The two stale "not exported" comments in ffi.rb
|
|
94
|
+
are gone; `leptris_xinclude_get_encoding` is bound.
|
|
95
|
+
- CI builds libleptris v1.1.0, whose SAX `LEPTRIS_API` annotations
|
|
96
|
+
(#430 fix) unblock Windows: all 9 matrix jobs expected green.
|
|
97
|
+
|
|
8
98
|
## [1.0.0] - 2026-08-21
|
|
9
99
|
|
|
10
100
|
The leptris rebrand, in lockstep with libleptris 1.0.0. Every
|
data/README.adoc
CHANGED
|
@@ -397,7 +397,7 @@ bundle exec rspec spec/xml/xpath_spec.rb:42 # one example by line
|
|
|
397
397
|
bundle exec rubocop # lint
|
|
398
398
|
----
|
|
399
399
|
|
|
400
|
-
CI pins libleptris to a released tag (currently
|
|
400
|
+
CI pins libleptris to a released tag (currently v1.1.1) and builds it
|
|
401
401
|
from source on each runner; see `.github/workflows/build.yml`.
|
|
402
402
|
|
|
403
403
|
== License
|
data/Rakefile
CHANGED
|
@@ -4,4 +4,95 @@ require "bundler/gem_tasks"
|
|
|
4
4
|
require "rspec/core/rake_task"
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
|
+
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
|
+
# CHANGELOG when libleptris releases.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.1.1"
|
|
12
|
+
|
|
13
|
+
CMAKE_FLAGS = %w[
|
|
14
|
+
-DCMAKE_BUILD_TYPE=Release
|
|
15
|
+
-DLEPTRIS_BUILD_SHARED=ON
|
|
16
|
+
-DLEPTRIS_BUILD_STATIC=OFF
|
|
17
|
+
-DBUILD_TESTING=OFF
|
|
18
|
+
-DLEPTRIS_BUILD_CLI=OFF
|
|
19
|
+
-DLEPTRIS_BUILD_BENCHMARKS=OFF
|
|
20
|
+
-DLEPTRIS_BUILD_MAN_PAGES=OFF
|
|
21
|
+
-DLEPTRIS_ENABLE_UTF8PROC=OFF
|
|
22
|
+
-DLEPTRIS_ENABLE_ICONV=OFF
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
desc "Build libleptris #{LIBLEPTRIS_VERSION} from its release tarball into lib/"
|
|
26
|
+
task :compile do
|
|
27
|
+
version = ENV.fetch("LIBLEPTRIS_VERSION", LIBLEPTRIS_VERSION)
|
|
28
|
+
build = File.expand_path("tmp/libleptris-#{version}", __dir__)
|
|
29
|
+
rm_rf(build)
|
|
30
|
+
mkdir_p(build)
|
|
31
|
+
url = "https://api.github.com/repos/leptris/leptris/tarball/v#{version}"
|
|
32
|
+
sh "curl -sL #{url} | tar xz -C #{build} --strip-components=1"
|
|
33
|
+
sh "cmake -B #{build}/build -S #{build} #{CMAKE_FLAGS.join(' ')}"
|
|
34
|
+
sh "cmake --build #{build}/build --config Release -j 4"
|
|
35
|
+
# Windows names the shared library leptris.dll (no "lib" prefix);
|
|
36
|
+
# vendoring under the uniform libleptris.* name keeps the FFI
|
|
37
|
+
# search order simple.
|
|
38
|
+
lib = Dir.glob("#{build}/build/**/libleptris.{dylib,so}").first ||
|
|
39
|
+
Dir.glob("#{build}/build/**/leptris.dll").first
|
|
40
|
+
raise "libleptris shared library not found after build" unless lib
|
|
41
|
+
ext = File.extname(lib)
|
|
42
|
+
cp(lib, "lib/libleptris#{ext}")
|
|
43
|
+
puts "Vendored #{File.basename(lib)} as lib/libleptris#{ext}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
task spec: :compile unless ENV.key?("LEPTRIS_LIB_PATH")
|
|
47
|
+
|
|
7
48
|
task default: :spec
|
|
49
|
+
|
|
50
|
+
require "rubygems/package_task"
|
|
51
|
+
|
|
52
|
+
desc "Build the pure-Ruby gem"
|
|
53
|
+
task "gem:native:any" do
|
|
54
|
+
sh "rake platform:any gem"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Define the gem task to build the pure-Ruby gem"
|
|
58
|
+
task "platform:any" do
|
|
59
|
+
spec = Gem::Specification::load("leptris.gemspec").dup
|
|
60
|
+
task = Gem::PackageTask.new(spec)
|
|
61
|
+
task.define
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
platforms = [
|
|
65
|
+
"x64-mingw32",
|
|
66
|
+
"x64-mingw-ucrt",
|
|
67
|
+
"aarch64-mingw-ucrt",
|
|
68
|
+
"x86_64-linux",
|
|
69
|
+
"x86_64-linux-musl",
|
|
70
|
+
"aarch64-linux",
|
|
71
|
+
"aarch64-linux-musl",
|
|
72
|
+
"x86_64-darwin",
|
|
73
|
+
"arm64-darwin",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
platforms.each do |platform|
|
|
77
|
+
desc "Build pre-compiled gem for the #{platform} platform"
|
|
78
|
+
task "gem:native:#{platform}" do
|
|
79
|
+
sh "rake compile platform:#{platform} gem"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "Define the gem task to build on the #{platform} platform (binary gem)"
|
|
83
|
+
task "platform:#{platform}" do
|
|
84
|
+
spec = Gem::Specification::load("leptris.gemspec").dup
|
|
85
|
+
spec.platform = Gem::Platform.new(platform)
|
|
86
|
+
spec.files += Dir.glob("lib/libleptris.{dll,so,dylib}")
|
|
87
|
+
task = Gem::PackageTask.new(spec)
|
|
88
|
+
task.define
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
require "rake/clean"
|
|
93
|
+
|
|
94
|
+
CLOBBER.include("pkg")
|
|
95
|
+
CLEAN.include("tmp",
|
|
96
|
+
"lib/libleptris.dll",
|
|
97
|
+
"lib/libleptris.dylib",
|
|
98
|
+
"lib/libleptris.so")
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/attr.rb
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Lightweight attribute wrapper
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# element via #[]. Once upstream adds attribute-level accessors, this can
|
|
7
|
-
# become a pointer-backed wrapper.
|
|
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 #[].
|
|
8
6
|
|
|
9
7
|
class Leptris::XML::Attr
|
|
10
8
|
attr_reader :name, :value, :element
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapter for libleptris's NULL-terminated char** wire format. Both
|
|
4
|
+
# directions of the conversion live here so the pointer arithmetic has
|
|
5
|
+
# one home (previously c14n built the array by hand and SAX walked it
|
|
6
|
+
# by hand).
|
|
7
|
+
module Leptris::XML::CStringArray
|
|
8
|
+
# Ruby strings -> NULL-terminated char**. Returns [buffer, anchors];
|
|
9
|
+
# the caller must keep both referenced for the duration of the FFI
|
|
10
|
+
# call (e.g. via local variables).
|
|
11
|
+
def self.to_c(strings)
|
|
12
|
+
strs = Array(strings).map(&:to_s)
|
|
13
|
+
return [nil, nil] if strs.empty?
|
|
14
|
+
|
|
15
|
+
ptr_size = ::FFI::type_size(:pointer)
|
|
16
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, strs.size + 1)
|
|
17
|
+
anchors = strs.map { |s| ::FFI::MemoryPointer.from_string(s) }
|
|
18
|
+
anchors.each_with_index { |p, i| buffer.put_pointer(i * ptr_size, p) }
|
|
19
|
+
buffer.put_pointer(strs.size * ptr_size, nil)
|
|
20
|
+
[buffer, anchors]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# NULL-terminated char** -> Ruby strings. Stops at the first NULL
|
|
24
|
+
# entry. Returns [] for a NULL pointer.
|
|
25
|
+
def self.to_ruby(ptr)
|
|
26
|
+
return [] if ptr.nil? || ptr.null?
|
|
27
|
+
|
|
28
|
+
ptr_size = ::FFI::type_size(:pointer)
|
|
29
|
+
result = []
|
|
30
|
+
offset = 0
|
|
31
|
+
while (entry = ptr.get_pointer(offset)) && !entry.null?
|
|
32
|
+
result << entry.read_string
|
|
33
|
+
offset += ptr_size
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/leptris/xml/cdata.rb
CHANGED
|
@@ -8,9 +8,8 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def content=(new_content)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
11
|
+
Leptris::XML::FFI.check_status(
|
|
12
|
+
Leptris::XML::FFI.leptris_cdata_node_set_content(@c_ptr, new_content.to_s))
|
|
14
13
|
new_content
|
|
15
14
|
end
|
|
16
15
|
end
|
data/lib/leptris/xml/comment.rb
CHANGED
|
@@ -8,9 +8,8 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def content=(new_content)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
11
|
+
Leptris::XML::FFI.check_status(
|
|
12
|
+
Leptris::XML::FFI.leptris_comment_node_set_content(@c_ptr, new_content.to_s))
|
|
14
13
|
new_content
|
|
15
14
|
end
|
|
16
15
|
end
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -17,20 +17,36 @@ class Leptris::XML::Document
|
|
|
17
17
|
def initialize(c_ptr = nil, freed = Freed.new(:alive))
|
|
18
18
|
@c_ptr = c_ptr
|
|
19
19
|
@freed = freed
|
|
20
|
-
# Per-document
|
|
21
|
-
# address.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
# Per-document STRONG cache for Node wrappers, keyed on c_ptr
|
|
21
|
+
# address. Every wrapper is created through Node.wrap, which is the
|
|
22
|
+
# single construction path, so the same C node always yields the
|
|
23
|
+
# same Ruby object. Cleared when the Document is freed — no stale
|
|
24
|
+
# entries.
|
|
25
|
+
#
|
|
26
|
+
# Deliberately NOT ObjectSpace::WeakMap: a weak cache makes wrapper
|
|
27
|
+
# identity a GC race. `doc.root.equal?(doc.root)` failed on the
|
|
28
|
+
# Windows CI matrix (188 examples, the 4 identity specs) because
|
|
29
|
+
# between the two calls the first wrapper was referenced only by
|
|
30
|
+
# the weak map — any GC sweep evicted it and the second call built
|
|
31
|
+
# a fresh object. A strong cache costs at most one wrapper per node
|
|
32
|
+
# actually visited, held until the document dies.
|
|
33
|
+
@wrapper_cache = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.parse(xml_or_io, options: nil)
|
|
28
37
|
xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
|
|
29
38
|
if xml.empty?
|
|
30
39
|
raise Leptris::XML::ParseError, "empty input"
|
|
31
40
|
end
|
|
41
|
+
flags = resolve_flags(options)
|
|
32
42
|
status_ptr = ::FFI::MemoryPointer.new(:int)
|
|
33
|
-
raw =
|
|
43
|
+
raw =
|
|
44
|
+
if flags.zero?
|
|
45
|
+
Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
|
|
46
|
+
else
|
|
47
|
+
Leptris::XML::FFI.leptris_parse_string_flags(
|
|
48
|
+
xml, xml.bytesize, flags, status_ptr)
|
|
49
|
+
end
|
|
34
50
|
if raw.null?
|
|
35
51
|
status = status_ptr.read_int
|
|
36
52
|
raise Leptris::XML::ParseError,
|
|
@@ -74,42 +90,51 @@ class Leptris::XML::Document
|
|
|
74
90
|
end
|
|
75
91
|
private_class_method :finalizer
|
|
76
92
|
|
|
93
|
+
def self.resolve_flags(options)
|
|
94
|
+
return Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT if options.nil?
|
|
95
|
+
unless options.is_a?(Leptris::XML::ParseOptions)
|
|
96
|
+
raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
|
|
97
|
+
end
|
|
98
|
+
options.flags
|
|
99
|
+
end
|
|
100
|
+
private_class_method :resolve_flags
|
|
101
|
+
|
|
77
102
|
def root
|
|
78
103
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
79
104
|
return nil if @c_ptr.nil?
|
|
80
105
|
ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
|
|
81
106
|
return nil if ptr.null?
|
|
82
|
-
Leptris::XML::
|
|
107
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
83
108
|
end
|
|
84
109
|
|
|
85
110
|
def create_element(name)
|
|
86
111
|
ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
|
|
87
112
|
raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
|
|
88
|
-
Leptris::XML::
|
|
113
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
89
114
|
end
|
|
90
115
|
|
|
91
116
|
def create_text_node(content)
|
|
92
117
|
ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
|
|
93
118
|
raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
|
|
94
|
-
Leptris::XML::
|
|
119
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
95
120
|
end
|
|
96
121
|
|
|
97
122
|
def create_comment(content)
|
|
98
123
|
ptr = Leptris::XML::FFI.leptris_comment_node_create(@c_ptr, content.to_s)
|
|
99
124
|
raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
|
|
100
|
-
Leptris::XML::
|
|
125
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
101
126
|
end
|
|
102
127
|
|
|
103
128
|
def create_cdata(content)
|
|
104
129
|
ptr = Leptris::XML::FFI.leptris_cdata_node_create(@c_ptr, content.to_s)
|
|
105
130
|
raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
|
|
106
|
-
Leptris::XML::
|
|
131
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
107
132
|
end
|
|
108
133
|
|
|
109
134
|
def create_processing_instruction(target, data = "")
|
|
110
135
|
ptr = Leptris::XML::FFI.leptris_pi_node_create(@c_ptr, target.to_s, data.to_s)
|
|
111
136
|
raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
|
|
112
|
-
Leptris::XML::
|
|
137
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
113
138
|
end
|
|
114
139
|
|
|
115
140
|
def fragment(markup)
|
|
@@ -133,22 +158,21 @@ class Leptris::XML::Document
|
|
|
133
158
|
def to_xml(indent: 0, no_decl: false, encoding: nil)
|
|
134
159
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
135
160
|
return "" if @c_ptr.nil?
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
str_ptr.read_string.tap { |s| Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
161
|
+
Leptris::XML::Serialization.to_xml(
|
|
162
|
+
Leptris::XML::FFI.method(:leptris_document_serialize), @c_ptr,
|
|
163
|
+
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
140
164
|
end
|
|
141
165
|
alias_method :to_s, :to_xml
|
|
142
166
|
alias_method :serialize, :to_xml
|
|
143
167
|
|
|
144
168
|
def save(path, **opts)
|
|
145
|
-
opts_struct,
|
|
169
|
+
opts_struct, _encoding_anchor = Leptris::XML::Serialization.build_options(
|
|
146
170
|
indent: opts.fetch(:indent, 0),
|
|
147
171
|
no_decl: opts.fetch(:no_decl, false),
|
|
148
172
|
encoding: opts[:encoding])
|
|
149
|
-
status = Leptris::XML::FFI.leptris_document_save_file(
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
status = Leptris::XML::FFI.leptris_document_save_file(
|
|
174
|
+
@c_ptr, path, opts_struct.pointer)
|
|
175
|
+
Leptris::XML::FFI.check_status(status)
|
|
152
176
|
self
|
|
153
177
|
end
|
|
154
178
|
|
|
@@ -161,12 +185,11 @@ class Leptris::XML::Document
|
|
|
161
185
|
return "" if @c_ptr.nil?
|
|
162
186
|
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
163
187
|
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
str_ptr.read_string.tap { Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
188
|
+
Leptris::XML::Serialization.canonicalize(
|
|
189
|
+
Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), @c_ptr,
|
|
190
|
+
version: version, mode: resolved_mode,
|
|
191
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
192
|
+
with_comments: with_comments)
|
|
170
193
|
end
|
|
171
194
|
alias_method :c14n, :canonicalize
|
|
172
195
|
|
|
@@ -175,6 +198,7 @@ class Leptris::XML::Document
|
|
|
175
198
|
@freed.state = :freed
|
|
176
199
|
Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
|
|
177
200
|
@c_ptr = nil
|
|
201
|
+
@wrapper_cache.clear
|
|
178
202
|
end
|
|
179
203
|
|
|
180
204
|
def name; "document"; end
|
|
@@ -184,19 +208,5 @@ class Leptris::XML::Document
|
|
|
184
208
|
Leptris::XML::FFI.leptris_document_encoding(@c_ptr)
|
|
185
209
|
end
|
|
186
210
|
|
|
187
|
-
private
|
|
188
|
-
|
|
189
|
-
def build_serialize_options(indent:, no_decl:, encoding:)
|
|
190
|
-
opts = Leptris::XML::FFI::SerializeOptions.new
|
|
191
|
-
opts[:indent] = indent.to_i
|
|
192
|
-
opts[:xml_declaration] = no_decl ? 0 : 1
|
|
193
|
-
enc_ptr = nil
|
|
194
|
-
if encoding
|
|
195
|
-
enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s)
|
|
196
|
-
opts[:encoding] = enc_ptr
|
|
197
|
-
end
|
|
198
|
-
[opts, enc_ptr]
|
|
199
|
-
end
|
|
200
|
-
|
|
201
211
|
include Leptris::XML::Searchable
|
|
202
212
|
end
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -7,9 +7,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
7
7
|
alias_method :node_name, :name
|
|
8
8
|
|
|
9
9
|
def name=(new_name)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
10
|
+
Leptris::XML::FFI.check_status(
|
|
11
|
+
Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name))
|
|
13
12
|
new_name
|
|
14
13
|
end
|
|
15
14
|
alias_method :node_name=, :name=
|
|
@@ -19,9 +18,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
def content=(new_content)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
21
|
+
Leptris::XML::FFI.check_status(
|
|
22
|
+
Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s))
|
|
25
23
|
new_content
|
|
26
24
|
end
|
|
27
25
|
|
|
@@ -32,95 +30,91 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
32
30
|
alias_method :get_attribute, :[]
|
|
33
31
|
|
|
34
32
|
def []=(key, value)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
33
|
+
Leptris::XML::FFI.check_status(
|
|
34
|
+
Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
|
|
38
35
|
value
|
|
39
36
|
end
|
|
40
37
|
alias_method :set_attribute, :[]=
|
|
41
38
|
|
|
42
39
|
def key?(name)
|
|
43
|
-
|
|
40
|
+
Leptris::XML::FFI.leptris_element_has_attribute(@c_ptr, name.to_s) != 0
|
|
44
41
|
end
|
|
45
42
|
alias_method :has_attribute?, :key?
|
|
46
43
|
|
|
47
44
|
def remove_attribute(name)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
45
|
+
Leptris::XML::FFI.check_status(
|
|
46
|
+
Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s))
|
|
51
47
|
self
|
|
52
48
|
end
|
|
53
49
|
alias_method :delete, :remove_attribute
|
|
54
50
|
|
|
51
|
+
# Iterates the element's attributes via the v1.1.0 linked-list face
|
|
52
|
+
# (one FFI call per attribute; the name_at/value_at indexing API
|
|
53
|
+
# re-walks the list per index, making it O(n^2) per element).
|
|
54
|
+
def each_attribute
|
|
55
|
+
return enum_for(:each_attribute) unless block_given?
|
|
56
|
+
attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
|
|
57
|
+
until attr.nil? || attr.null?
|
|
58
|
+
name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
|
|
59
|
+
value = Leptris::XML::FFI.leptris_attribute_get_value(@c_ptr, attr)
|
|
60
|
+
yield Leptris::XML::Attr.new(name, value, self)
|
|
61
|
+
attr = Leptris::XML::FFI.leptris_attribute_next(attr)
|
|
62
|
+
end
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
55
66
|
def keys
|
|
56
|
-
|
|
57
|
-
count.times.map { |i| Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i) }
|
|
67
|
+
each_attribute.to_a.map(&:name)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
def values
|
|
61
|
-
|
|
62
|
-
count.times.map { |i| Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i) }
|
|
71
|
+
each_attribute.to_a.map(&:value)
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
def attributes
|
|
66
|
-
count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
|
|
67
75
|
result = {}
|
|
68
|
-
|
|
69
|
-
name = Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i)
|
|
70
|
-
value = Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i)
|
|
71
|
-
result[name] = Leptris::XML::Attr.new(name, value, self)
|
|
72
|
-
end
|
|
76
|
+
each_attribute { |attr| result[attr.name] = attr }
|
|
73
77
|
result
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def attribute_nodes
|
|
77
|
-
|
|
78
|
-
count.times.map do |i|
|
|
79
|
-
name = Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i)
|
|
80
|
-
value = Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i)
|
|
81
|
-
Leptris::XML::Attr.new(name, value, self)
|
|
82
|
-
end
|
|
81
|
+
each_attribute.to_a
|
|
83
82
|
end
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
node
|
|
84
|
+
# The element's own namespace prefix (e.g. "foo" for <foo:child/>),
|
|
85
|
+
# or nil when the element has none.
|
|
86
|
+
def prefix
|
|
87
|
+
Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
|
|
90
88
|
end
|
|
91
|
-
alias_method :<<, :add_child
|
|
92
89
|
|
|
93
90
|
def prepend_child(node)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
91
|
+
Leptris::XML::FFI.check_status(
|
|
92
|
+
Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
|
|
97
93
|
node
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def add_next_sibling(node)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
97
|
+
Leptris::XML::FFI.check_status(
|
|
98
|
+
Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
|
|
104
99
|
node
|
|
105
100
|
end
|
|
106
101
|
|
|
107
102
|
def add_previous_sibling(node)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
103
|
+
Leptris::XML::FFI.check_status(
|
|
104
|
+
Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
|
|
111
105
|
node
|
|
112
106
|
end
|
|
113
107
|
|
|
114
108
|
def remove_child(node)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
109
|
+
Leptris::XML::FFI.check_status(
|
|
110
|
+
Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr))
|
|
118
111
|
node
|
|
119
112
|
end
|
|
120
113
|
|
|
121
114
|
def children=(node_or_nodes)
|
|
122
115
|
# Remove existing children, then attach the new ones in source order.
|
|
123
|
-
Leptris::XML::FFI.
|
|
116
|
+
Leptris::XML::FFI.check_status(
|
|
117
|
+
Leptris::XML::FFI.leptris_element_remove_children(@c_ptr))
|
|
124
118
|
Array(node_or_nodes).each { |n| add_child(n) }
|
|
125
119
|
end
|
|
126
120
|
|
|
@@ -169,24 +163,22 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
169
163
|
def dup
|
|
170
164
|
copy_ptr = Leptris::XML::FFI.leptris_element_copy(@c_ptr, @document.c_ptr)
|
|
171
165
|
raise Leptris::XML::Error, "leptris_element_copy failed" if copy_ptr.null?
|
|
172
|
-
Leptris::XML::
|
|
166
|
+
Leptris::XML::Node.wrap(copy_ptr, @document)
|
|
173
167
|
end
|
|
174
168
|
alias_method :clone, :dup
|
|
175
169
|
|
|
176
170
|
def add_child(node_or_markup)
|
|
177
171
|
case node_or_markup
|
|
178
172
|
when Leptris::XML::Node
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
173
|
+
Leptris::XML::FFI.check_status(
|
|
174
|
+
Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
|
|
182
175
|
node_or_markup
|
|
183
176
|
when String
|
|
184
177
|
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
185
178
|
added = []
|
|
186
179
|
frag.children.each do |n|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
180
|
+
Leptris::XML::FFI.check_status(
|
|
181
|
+
Leptris::XML::FFI.leptris_element_append_child(@c_ptr, n.c_ptr))
|
|
190
182
|
added << n
|
|
191
183
|
end
|
|
192
184
|
Leptris::XML::NodeSet.new(@document, added)
|
|
@@ -194,6 +186,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
194
186
|
raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
|
|
195
187
|
end
|
|
196
188
|
end
|
|
189
|
+
alias_method :<<, :add_child
|
|
197
190
|
|
|
198
191
|
def namespace
|
|
199
192
|
uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
@@ -224,17 +217,9 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
224
217
|
end
|
|
225
218
|
|
|
226
219
|
def to_xml(indent: 0, no_decl: false, encoding: nil)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
enc_ptr = nil
|
|
231
|
-
if encoding
|
|
232
|
-
enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s)
|
|
233
|
-
opts[:encoding] = enc_ptr
|
|
234
|
-
end
|
|
235
|
-
str_ptr = Leptris::XML::FFI.leptris_element_serialize(@c_ptr, opts.pointer)
|
|
236
|
-
return "" if str_ptr.null?
|
|
237
|
-
str_ptr.read_string.tap { Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
220
|
+
Leptris::XML::Serialization.to_xml(
|
|
221
|
+
Leptris::XML::FFI.method(:leptris_element_serialize), @c_ptr,
|
|
222
|
+
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
238
223
|
end
|
|
239
224
|
|
|
240
225
|
def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
|
|
@@ -244,35 +229,31 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
244
229
|
mode: nil)
|
|
245
230
|
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
246
231
|
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
str_ptr.read_string.tap { Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
232
|
+
Leptris::XML::Serialization.canonicalize(
|
|
233
|
+
Leptris::XML::FFI.method(:leptris_c14n_canonicalize_subtree_ex), @c_ptr,
|
|
234
|
+
version: version, mode: resolved_mode,
|
|
235
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
236
|
+
with_comments: with_comments)
|
|
253
237
|
end
|
|
254
238
|
alias_method :c14n, :canonicalize
|
|
255
239
|
|
|
256
240
|
def add_namespace_definition(prefix, href)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
241
|
+
Leptris::XML::FFI.check_status(
|
|
242
|
+
Leptris::XML::FFI.leptris_element_add_namespace_definition(
|
|
243
|
+
@c_ptr, prefix.to_s, href.to_s))
|
|
261
244
|
Leptris::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s)
|
|
262
245
|
end
|
|
263
246
|
alias_method :add_namespace, :add_namespace_definition
|
|
264
247
|
|
|
265
248
|
def default_namespace=(href)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
249
|
+
Leptris::XML::FFI.check_status(
|
|
250
|
+
Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s))
|
|
269
251
|
href
|
|
270
252
|
end
|
|
271
253
|
|
|
272
254
|
def remove_namespace_definition(prefix)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
255
|
+
Leptris::XML::FFI.check_status(
|
|
256
|
+
Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s))
|
|
276
257
|
self
|
|
277
258
|
end
|
|
278
259
|
end
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Leptris
|
|
|
11
11
|
ENV["LEPTRIS_LIB_PATH"],
|
|
12
12
|
File.expand_path("../../libleptris.dylib", __dir__),
|
|
13
13
|
File.expand_path("../../libleptris.so", __dir__),
|
|
14
|
+
File.expand_path("../../libleptris.dll", __dir__),
|
|
14
15
|
"/usr/local/lib/libleptris.dylib",
|
|
15
16
|
"/usr/local/lib/libleptris.so",
|
|
16
17
|
"leptris",
|
|
@@ -53,6 +54,8 @@ module Leptris
|
|
|
53
54
|
|
|
54
55
|
attach_function :leptris_parse_string,
|
|
55
56
|
[:string, :size_t, :pointer], :leptris_document
|
|
57
|
+
attach_function :leptris_parse_string_flags,
|
|
58
|
+
[:string, :size_t, :uint, :pointer], :leptris_document
|
|
56
59
|
attach_function :leptris_parse_string_inplace,
|
|
57
60
|
[:pointer, :size_t, :pointer], :leptris_document
|
|
58
61
|
attach_function :leptris_parse_string_with_encoding,
|
|
@@ -92,9 +95,7 @@ module Leptris
|
|
|
92
95
|
attach_function :leptris_xinclude_get_href, [:leptris_element], :string
|
|
93
96
|
attach_function :leptris_xinclude_get_parse, [:leptris_element], :string
|
|
94
97
|
attach_function :leptris_xinclude_get_xpointer, [:leptris_element], :string
|
|
95
|
-
|
|
96
|
-
# from the v0.4.4 dylib (likely same visibility bug as the serialize
|
|
97
|
-
# functions; see upstream issue #166). Re-add once #173 / v0.4.5 ships.
|
|
98
|
+
attach_function :leptris_xinclude_get_encoding, [:leptris_element], :string
|
|
98
99
|
|
|
99
100
|
attach_function :leptris_node_get_type,
|
|
100
101
|
[:leptris_node_ref], :int
|
|
@@ -184,9 +185,18 @@ module Leptris
|
|
|
184
185
|
[:leptris_element, :string, :float], :float
|
|
185
186
|
attach_function :leptris_element_attribute_bool,
|
|
186
187
|
[:leptris_element, :string, :int], :int
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
attach_function :leptris_element_has_attribute,
|
|
189
|
+
[:leptris_element, :string], :int
|
|
190
|
+
attach_function :leptris_element_first_attribute,
|
|
191
|
+
[:leptris_element], :leptris_attribute
|
|
192
|
+
attach_function :leptris_attribute_next,
|
|
193
|
+
[:leptris_attribute], :leptris_attribute
|
|
194
|
+
attach_function :leptris_attribute_get_name,
|
|
195
|
+
[:leptris_attribute], :string
|
|
196
|
+
attach_function :leptris_attribute_get_value,
|
|
197
|
+
[:leptris_element, :leptris_attribute], :string
|
|
198
|
+
attach_function :leptris_element_prefix,
|
|
199
|
+
[:leptris_element], :string
|
|
190
200
|
attach_function :leptris_element_attribute_count,
|
|
191
201
|
[:leptris_element], :size_t
|
|
192
202
|
attach_function :leptris_element_attribute_name_at,
|
|
@@ -311,6 +321,22 @@ module Leptris
|
|
|
311
321
|
[:leptris_xpath_result], :size_t
|
|
312
322
|
attach_function :leptris_xpath_result_get,
|
|
313
323
|
[:leptris_xpath_result, :size_t], :leptris_element
|
|
324
|
+
# Mixed nodesets (v1.1.0): unlike result_get (elements only),
|
|
325
|
+
# get_node returns the node whatever its kind; node_kind reports
|
|
326
|
+
# which (element / synthetic attribute / text / other).
|
|
327
|
+
attach_function :leptris_xpath_result_node_kind,
|
|
328
|
+
[:leptris_xpath_result, :size_t], :int
|
|
329
|
+
attach_function :leptris_xpath_result_get_node,
|
|
330
|
+
[:leptris_xpath_result, :size_t], :leptris_node_ref
|
|
331
|
+
attach_function :leptris_xpath_result_node_name,
|
|
332
|
+
[:leptris_xpath_result, :size_t], :string
|
|
333
|
+
attach_function :leptris_xpath_result_node_value,
|
|
334
|
+
[:leptris_xpath_result, :size_t], :string
|
|
335
|
+
# Document-scoped custom XPath functions. The callback receives
|
|
336
|
+
# (const char* const* args, int argc, void* user_data) and returns
|
|
337
|
+
# a heap string the library frees.
|
|
338
|
+
attach_function :leptris_xpath_register_function,
|
|
339
|
+
[:leptris_document, :string, :pointer, :pointer], :leptris_status
|
|
314
340
|
attach_function :leptris_xpath_result_get_nodes,
|
|
315
341
|
[:leptris_xpath_result, :pointer, :size_t], :size_t
|
|
316
342
|
attach_function :leptris_xpath_result_boolean,
|
|
@@ -348,6 +374,12 @@ module Leptris
|
|
|
348
374
|
|
|
349
375
|
attach_function :leptris_document_serialize,
|
|
350
376
|
[:leptris_document, :pointer], :pointer
|
|
377
|
+
# Legacy simple serializer, declared retroactively in v1.1.0.
|
|
378
|
+
# Prefer leptris_document_serialize; bound for API completeness.
|
|
379
|
+
attach_function :leptris_serialize_document,
|
|
380
|
+
[:leptris_document], :pointer
|
|
381
|
+
attach_function :leptris_document_get_dtd,
|
|
382
|
+
[:leptris_document], :pointer
|
|
351
383
|
attach_function :leptris_element_serialize,
|
|
352
384
|
[:leptris_element, :pointer], :pointer
|
|
353
385
|
attach_function :leptris_document_save_file,
|
|
@@ -399,6 +431,11 @@ module Leptris
|
|
|
399
431
|
XPATH_NUMBER = 2
|
|
400
432
|
XPATH_STRING = 3
|
|
401
433
|
|
|
434
|
+
XPATH_NODE_ELEMENT = 0
|
|
435
|
+
XPATH_NODE_ATTRIBUTE = 1
|
|
436
|
+
XPATH_NODE_TEXT = 2
|
|
437
|
+
XPATH_NODE_OTHER = 3
|
|
438
|
+
|
|
402
439
|
NODE_ELEMENT = 0
|
|
403
440
|
NODE_TEXT = 1
|
|
404
441
|
NODE_COMMENT = 2
|
|
@@ -415,6 +452,24 @@ module Leptris
|
|
|
415
452
|
|
|
416
453
|
TRAVERSE_PRE_ORDER = 0
|
|
417
454
|
TRAVERSE_POST_ORDER = 1
|
|
455
|
+
|
|
456
|
+
LEPTRIS_PARSE_DEFAULT = 0
|
|
457
|
+
LEPTRIS_PARSE_DROP_WS_TEXT = 1
|
|
458
|
+
|
|
459
|
+
# Reads an libleptris-owned char* result and frees it as one unit,
|
|
460
|
+
# so a call site can neither leak nor double-free.
|
|
461
|
+
def self.read_owned_string(ptr)
|
|
462
|
+
return "" if ptr.nil? || ptr.null?
|
|
463
|
+
ptr.read_string.tap { leptris_free_string(ptr) }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Single status seam: turns a C status code into a Ruby error.
|
|
467
|
+
def self.check_status(status)
|
|
468
|
+
unless status == LEPTRIS_OK
|
|
469
|
+
raise Leptris::XML::Error, leptris_status_string(status)
|
|
470
|
+
end
|
|
471
|
+
status
|
|
472
|
+
end
|
|
418
473
|
end
|
|
419
474
|
end
|
|
420
475
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -66,7 +66,7 @@ class Leptris::XML::Node
|
|
|
66
66
|
return @parent if @parent
|
|
67
67
|
ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
|
|
68
68
|
return nil if ptr.null?
|
|
69
|
-
Leptris::XML::
|
|
69
|
+
Leptris::XML::Node.wrap(ptr, @document)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def line
|
|
@@ -141,9 +141,8 @@ class Leptris::XML::Node
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def unlink
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
144
|
+
Leptris::XML::FFI.check_status(
|
|
145
|
+
Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
|
|
147
146
|
@parent = nil
|
|
148
147
|
self
|
|
149
148
|
end
|
|
@@ -173,7 +172,7 @@ class Leptris::XML::Node
|
|
|
173
172
|
def path
|
|
174
173
|
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
|
|
175
174
|
return nil if str_ptr.null?
|
|
176
|
-
|
|
175
|
+
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
177
176
|
end
|
|
178
177
|
|
|
179
178
|
def css_path
|
|
@@ -189,7 +188,7 @@ class Leptris::XML::Node
|
|
|
189
188
|
raise Leptris::XML::Error, "dup is only supported for element nodes" if elem_ptr.null?
|
|
190
189
|
copy_ptr = Leptris::XML::FFI.leptris_element_copy(elem_ptr, @document.c_ptr)
|
|
191
190
|
raise Leptris::XML::Error, "leptris_element_copy failed" if copy_ptr.null?
|
|
192
|
-
Leptris::XML::
|
|
191
|
+
Leptris::XML::Node.wrap(copy_ptr, @document)
|
|
193
192
|
end
|
|
194
193
|
alias_method :clone, :dup
|
|
195
194
|
|
|
@@ -208,14 +207,5 @@ class Leptris::XML::Node
|
|
|
208
207
|
is_a?(Leptris::XML::Element) ? self : nil
|
|
209
208
|
end
|
|
210
209
|
|
|
211
|
-
private
|
|
212
|
-
|
|
213
|
-
# Walks the subtree in post-order DFS (matches Nokogiri's semantics).
|
|
214
|
-
#
|
|
215
|
-
# Specialized hot path for #traverse: skips the intermediate NodeSet
|
|
216
|
-
# allocation that Element#children would create, walking via raw FFI
|
|
217
|
-
# calls and wrapping nodes directly. Saves one Array + one NodeSet
|
|
218
|
-
# allocation per parent node.
|
|
219
|
-
#
|
|
220
210
|
include Leptris::XML::Searchable
|
|
221
211
|
end
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -52,7 +52,7 @@ class Leptris::XML::NodeSet
|
|
|
52
52
|
def [](idx)
|
|
53
53
|
if @result_ptr
|
|
54
54
|
return nil if idx < 0 || idx >= length
|
|
55
|
-
ptr = Leptris::XML::FFI.
|
|
55
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
|
|
56
56
|
return nil if ptr.null?
|
|
57
57
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
58
58
|
else
|
|
@@ -77,6 +77,13 @@ class Leptris::XML::NodeSet
|
|
|
77
77
|
next if ptr.null?
|
|
78
78
|
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
79
79
|
end
|
|
80
|
+
# The batch accessor under-copies mixed-kind results (upstream
|
|
81
|
+
# leptris#477); fall back to per-index fetch for the remainder.
|
|
82
|
+
(copied...n).each do |i|
|
|
83
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
84
|
+
next if ptr.null?
|
|
85
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
86
|
+
end
|
|
80
87
|
ensure
|
|
81
88
|
buf.free
|
|
82
89
|
end
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Parser flags, mirroring LeptrisParseFlags in libleptris's public
|
|
4
|
+
# headers. Pass an instance as `Leptris::XML.parse(xml, options:)`.
|
|
3
5
|
class Leptris::XML::ParseOptions
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
STRICT = 1 << 18
|
|
6
|
+
# Discard whitespace-only text nodes (runs between tags that contain
|
|
7
|
+
# nothing but spaces/tabs/newlines). Matches libxml2's
|
|
8
|
+
# XML_PARSE_NOBLANKS and Nokogiri's noblanks. Documents parsed this
|
|
9
|
+
# way do not round-trip pretty-printed formatting byte-for-byte.
|
|
10
|
+
NOBLANKS = Leptris::XML::FFI::LEPTRIS_PARSE_DROP_WS_TEXT
|
|
10
11
|
|
|
11
|
-
attr_reader :
|
|
12
|
+
attr_reader :flags
|
|
12
13
|
|
|
13
|
-
def initialize(
|
|
14
|
-
@
|
|
14
|
+
def initialize(flags = Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT)
|
|
15
|
+
@flags = flags.to_i
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
-
|
|
18
|
+
def self.noblanks
|
|
19
|
+
new(NOBLANKS)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def noblanks?
|
|
23
|
+
@flags & NOBLANKS != 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def |(other)
|
|
27
|
+
self.class.new(@flags | other.flags)
|
|
28
|
+
end
|
|
19
29
|
end
|
|
@@ -11,16 +11,14 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def target=(new_target)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
14
|
+
Leptris::XML::FFI.check_status(
|
|
15
|
+
Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s))
|
|
17
16
|
new_target
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
def data=(new_data)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
20
|
+
Leptris::XML::FFI.check_status(
|
|
21
|
+
Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s))
|
|
24
22
|
new_data
|
|
25
23
|
end
|
|
26
24
|
end
|
|
@@ -128,21 +128,9 @@ class Leptris::XML::SAX::Parser
|
|
|
128
128
|
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# The C `const char** attrs` is a NULL-terminated flat array of
|
|
132
|
-
# pairs
|
|
131
|
+
# The C `const char** attrs` is a NULL-terminated flat array of
|
|
132
|
+
# name/value pairs; CStringArray owns the format, we slice into pairs.
|
|
133
133
|
def walk_attr_array(attrs_ptr)
|
|
134
|
-
|
|
135
|
-
ptr_size = ::FFI.type_size(:pointer)
|
|
136
|
-
result = []
|
|
137
|
-
offset = 0
|
|
138
|
-
loop do
|
|
139
|
-
name_ptr = attrs_ptr.get_pointer(offset)
|
|
140
|
-
break if name_ptr.null?
|
|
141
|
-
value_ptr = attrs_ptr.get_pointer(offset + ptr_size)
|
|
142
|
-
break if value_ptr.null?
|
|
143
|
-
result << [name_ptr.read_string, value_ptr.read_string]
|
|
144
|
-
offset += 2 * ptr_size
|
|
145
|
-
end
|
|
146
|
-
result
|
|
134
|
+
Leptris::XML::CStringArray.to_ruby(attrs_ptr).each_slice(2).to_a
|
|
147
135
|
end
|
|
148
136
|
end
|
|
@@ -81,8 +81,7 @@ module Leptris::XML::Searchable
|
|
|
81
81
|
v
|
|
82
82
|
when Leptris::XML::FFI::XPATH_STRING
|
|
83
83
|
str_ptr = Leptris::XML::FFI.leptris_xpath_result_string(result_ptr)
|
|
84
|
-
v =
|
|
85
|
-
Leptris::XML::FFI.leptris_free_string(str_ptr) unless str_ptr.null?
|
|
84
|
+
v = Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
86
85
|
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
87
86
|
v
|
|
88
87
|
else
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Owns the SerializeOptions lifecycle and the owned-string read for
|
|
4
|
+
# document and subtree serialization/canonicalization. Document and
|
|
5
|
+
# Element expose one-line methods over this module — the options
|
|
6
|
+
# struct, the encoding anchor, the C call, and the read-and-free all
|
|
7
|
+
# live here.
|
|
8
|
+
module Leptris::XML::Serialization
|
|
9
|
+
# +ffi_function+ is a bound FFI function taking (c_ptr, opts_ptr):
|
|
10
|
+
# leptris_document_serialize or leptris_element_serialize.
|
|
11
|
+
def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
|
|
12
|
+
opts, _encoding_anchor = build_options(
|
|
13
|
+
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
14
|
+
str_ptr = ffi_function.call(c_ptr, opts.pointer)
|
|
15
|
+
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# +ffi_function+ is a bound FFI function taking
|
|
19
|
+
# (c_ptr, version, mode, ns_ptr, flags): leptris_c14n_canonicalize_ex
|
|
20
|
+
# or leptris_c14n_canonicalize_subtree_ex.
|
|
21
|
+
def self.canonicalize(ffi_function, c_ptr, version:, mode:,
|
|
22
|
+
inclusive_namespaces: nil, with_comments: false)
|
|
23
|
+
ns_ptr, _ns_anchor = Leptris::XML::CStringArray.to_c(inclusive_namespaces)
|
|
24
|
+
str_ptr = ffi_function.call(c_ptr, version, mode, ns_ptr, with_comments ? 1 : 0)
|
|
25
|
+
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds a SerializeOptions struct. Returns [opts, encoding_anchor];
|
|
29
|
+
# the anchor must stay referenced while opts is in an FFI call.
|
|
30
|
+
def self.build_options(indent: 0, no_decl: false, encoding: nil)
|
|
31
|
+
opts = Leptris::XML::FFI::SerializeOptions.new
|
|
32
|
+
opts[:indent] = indent.to_i
|
|
33
|
+
opts[:xml_declaration] = no_decl ? 0 : 1
|
|
34
|
+
encoding_anchor = nil
|
|
35
|
+
if encoding
|
|
36
|
+
encoding_anchor = ::FFI::MemoryPointer.from_string(encoding.to_s)
|
|
37
|
+
opts[:encoding] = encoding_anchor
|
|
38
|
+
end
|
|
39
|
+
[opts, encoding_anchor]
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -8,9 +8,8 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def content=(new_content)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
11
|
+
Leptris::XML::FFI.check_status(
|
|
12
|
+
Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s))
|
|
14
13
|
new_content
|
|
15
14
|
end
|
|
16
15
|
end
|
data/lib/leptris/xml.rb
CHANGED
|
@@ -18,13 +18,14 @@ module Leptris
|
|
|
18
18
|
autoload :Searchable, "leptris/xml/searchable"
|
|
19
19
|
autoload :ParseOptions, "leptris/xml/parse_options"
|
|
20
20
|
autoload :CssToXPath, "leptris/xml/css_to_xpath"
|
|
21
|
+
autoload :CStringArray, "leptris/xml/c_string_array"
|
|
22
|
+
autoload :Serialization, "leptris/xml/serialization"
|
|
21
23
|
autoload :SAX, "leptris/xml/sax"
|
|
22
|
-
require "leptris/xml/c14n" # module-level helper, eager load
|
|
23
24
|
|
|
24
25
|
# Nokogiri-style top-level entry points (Nokogiri::XML(...) /
|
|
25
26
|
# Nokogiri::XML.parse). Delegates to Document.parse.
|
|
26
|
-
def self.parse(xml_or_io)
|
|
27
|
-
Document.parse(xml_or_io)
|
|
27
|
+
def self.parse(xml_or_io, options: nil)
|
|
28
|
+
Document.parse(xml_or_io, options: options)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def self.parse_file(path)
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-08-23 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: ffi
|
|
@@ -111,7 +112,7 @@ files:
|
|
|
111
112
|
- lib/leptris/version.rb
|
|
112
113
|
- lib/leptris/xml.rb
|
|
113
114
|
- lib/leptris/xml/attr.rb
|
|
114
|
-
- lib/leptris/xml/
|
|
115
|
+
- lib/leptris/xml/c_string_array.rb
|
|
115
116
|
- lib/leptris/xml/cdata.rb
|
|
116
117
|
- lib/leptris/xml/comment.rb
|
|
117
118
|
- lib/leptris/xml/css_to_xpath.rb
|
|
@@ -129,6 +130,7 @@ files:
|
|
|
129
130
|
- lib/leptris/xml/sax/document.rb
|
|
130
131
|
- lib/leptris/xml/sax/parser.rb
|
|
131
132
|
- lib/leptris/xml/searchable.rb
|
|
133
|
+
- lib/leptris/xml/serialization.rb
|
|
132
134
|
- lib/leptris/xml/text.rb
|
|
133
135
|
homepage: https://github.com/leptris/leptris-ruby
|
|
134
136
|
licenses:
|
|
@@ -137,6 +139,7 @@ metadata:
|
|
|
137
139
|
homepage_uri: https://github.com/leptris/leptris-ruby
|
|
138
140
|
source_code_uri: https://github.com/leptris/leptris-ruby
|
|
139
141
|
changelog_uri: https://github.com/leptris/leptris-ruby/blob/main/CHANGELOG.md
|
|
142
|
+
post_install_message:
|
|
140
143
|
rdoc_options: []
|
|
141
144
|
require_paths:
|
|
142
145
|
- lib
|
|
@@ -151,7 +154,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
151
154
|
- !ruby/object:Gem::Version
|
|
152
155
|
version: '0'
|
|
153
156
|
requirements: []
|
|
154
|
-
rubygems_version:
|
|
157
|
+
rubygems_version: 3.5.22
|
|
158
|
+
signing_key:
|
|
155
159
|
specification_version: 4
|
|
156
160
|
summary: Nokogiri-compatible Ruby binding for libleptris (XML 1.0, XPath 1.0, SAX)
|
|
157
161
|
test_files: []
|
data/lib/leptris/xml/c14n.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Leptris
|
|
4
|
-
module XML
|
|
5
|
-
# @api private
|
|
6
|
-
#
|
|
7
|
-
# Builds a NULL-terminated `const char**` from a Ruby array of strings,
|
|
8
|
-
# suitable for passing to leptris_c14n_canonicalize_ex's
|
|
9
|
-
# `inclusive_ns_prefixes` argument. Returns [pointer, anchor] where
|
|
10
|
-
# anchor is the underlying MemoryPointer that the caller must keep
|
|
11
|
-
# alive (e.g. via local variable) for the duration of the FFI call.
|
|
12
|
-
def self.c14n_build_ns_pointer(inclusive_namespaces)
|
|
13
|
-
return [nil, nil] if inclusive_namespaces.nil? || inclusive_namespaces.empty?
|
|
14
|
-
strs = Array(inclusive_namespaces).map(&:to_s)
|
|
15
|
-
ptr_size = ::FFI.type_size(:pointer)
|
|
16
|
-
buffer = ::FFI::MemoryPointer.new(:pointer, strs.size + 1)
|
|
17
|
-
anchors = strs.map { |s| ::FFI::MemoryPointer.from_string(s) }
|
|
18
|
-
anchors.each_with_index { |p, i| buffer.put_pointer(i * ptr_size, p) }
|
|
19
|
-
buffer.put_pointer(strs.size * ptr_size, nil)
|
|
20
|
-
[buffer, anchors]
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|