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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59ba9b4cfe2de60b9ef9a20dc7bd607cc8f5bbeabb8ad3ff8f1df7f8a2b8dfb4
4
- data.tar.gz: 18e0b232c34140548b4908f6532ad193a7e3b3ec7255c918ad4ed624f75935bb
3
+ metadata.gz: 949fe73711448bd7857072004c75d6201b06829e9eb632124ae922a2fb7ff3f1
4
+ data.tar.gz: 259bbac7ce3e90389cce4f0e79dc979e7bc8ca844caa7745680ba352c010ba86
5
5
  SHA512:
6
- metadata.gz: e57a3704d91f60a43d5b7d1b071ac34359c6be05eb5ac324c81b36871e8bcf310cbf650bc6f814cd4724ed4c00f07d22b44cf190456ec9852fda265edef16c75
7
- data.tar.gz: 2127edf5b84e7ea8322d00b030bb5a60b98feca626bd10863a2a255882673bf1615c3408724fb486d338f9316c5a745e04e8a63538717d3668a4b96217a76f23
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 v0.26.7) and builds it
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")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.2"
5
5
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Lightweight attribute wrapper. libleptris v0.4.4 doesn't expose per-attribute
4
- # pointers (no LeptrisAttribute accessors in the public API), so an Attr is a
5
- # (name, value, parent_element) triple. Mutation goes through the parent
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
@@ -8,9 +8,8 @@ class Leptris::XML::CDATA < Leptris::XML::Text
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
- status = Leptris::XML::FFI.leptris_cdata_node_set_content(@c_ptr, new_content.to_s)
12
- raise Leptris::XML::Error,
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
@@ -8,9 +8,8 @@ class Leptris::XML::Comment < Leptris::XML::Node
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
- status = Leptris::XML::FFI.leptris_comment_node_set_content(@c_ptr, new_content.to_s)
12
- raise Leptris::XML::Error,
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
@@ -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 weak-ref cache for Node wrappers, keyed on c_ptr
21
- # address. Eliminates re-allocation when the same node is accessed
22
- # repeatedly (e.g. via children, siblings, multiple xpath calls).
23
- # Dies with the Document — no stale entries pointing at freed memory.
24
- @wrapper_cache = ObjectSpace::WeakMap.new
25
- end
26
-
27
- def self.parse(xml_or_io)
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 = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
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::Element.new(ptr, self)
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::Element.new(ptr, self)
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::Text.new(ptr, self)
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::Comment.new(ptr, self)
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::CDATA.new(ptr, self)
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::ProcessingInstruction.new(ptr, self)
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
- opts, enc_ptr = build_serialize_options(indent: indent, no_decl: no_decl, encoding: encoding)
137
- str_ptr = Leptris::XML::FFI.leptris_document_serialize(@c_ptr, opts.pointer)
138
- return "" if str_ptr.null?
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, enc_ptr = build_serialize_options(
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(@c_ptr, path, opts_struct.pointer)
150
- raise Leptris::XML::Error,
151
- Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
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
- ns_ptr, _anchor = Leptris::XML.c14n_build_ns_pointer(inclusive_namespaces)
165
- flags = with_comments ? 1 : 0
166
- str_ptr = Leptris::XML::FFI.leptris_c14n_canonicalize_ex(
167
- @c_ptr, version, resolved_mode, ns_ptr, flags)
168
- return "" if str_ptr.null?
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
@@ -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
- status = Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name)
11
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s)
23
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s)
36
- raise Leptris::XML::Error,
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
- !Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name.to_s).nil?
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
- status = Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s)
49
- raise Leptris::XML::Error,
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
- count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
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
- count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
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
- count.times do |i|
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
- count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
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
- def add_child(node)
86
- status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node.c_ptr)
87
- raise Leptris::XML::Error,
88
- Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
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
- status = Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr)
95
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr)
102
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr)
109
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr)
116
- raise Leptris::XML::Error,
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.leptris_element_remove_children(@c_ptr)
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::Element.new(copy_ptr, @document)
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
- status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr)
180
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, n.c_ptr)
188
- raise Leptris::XML::Error,
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
- opts = Leptris::XML::FFI::SerializeOptions.new
228
- opts[:indent] = indent.to_i
229
- opts[:xml_declaration] = no_decl ? 0 : 1
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
- ns_ptr, _anchor = Leptris::XML.c14n_build_ns_pointer(inclusive_namespaces)
248
- flags = with_comments ? 1 : 0
249
- str_ptr = Leptris::XML::FFI.leptris_c14n_canonicalize_subtree_ex(
250
- @c_ptr, version, resolved_mode, ns_ptr, flags)
251
- return "" if str_ptr.null?
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
- status = Leptris::XML::FFI.leptris_element_add_namespace_definition(
258
- @c_ptr, prefix.to_s, href.to_s)
259
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s)
267
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s)
274
- raise Leptris::XML::Error,
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
@@ -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
- # leptris_xinclude_get_encoding is declared in leptris.h but not exported
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
- # leptris_element_has_attribute is declared in leptris.h but not exported
188
- # from the v0.4.4 dylib (likely visibility bug; see upstream #166).
189
- # Ruby-level Element#key? will emulate via attribute lookup.
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
@@ -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::Element.new(ptr, @document)
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
- status = Leptris::XML::FFI.leptris_node_unlink(@c_ptr)
145
- raise Leptris::XML::Error,
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
- str_ptr.read_string.tap { Leptris::XML::FFI.leptris_free_string(str_ptr) }
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::Element.new(copy_ptr, @document)
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
@@ -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.leptris_xpath_result_get(@result_ptr, idx)
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
- DEFAULT_XML = 0
5
- RECOVER = 1 << 0
6
- NOERROR = 1 << 5
7
- NOWARNING = 1 << 6
8
- NOCDATA = 1 << 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 :options
12
+ attr_reader :flags
12
13
 
13
- def initialize(options = DEFAULT_XML)
14
- @options = options.to_i
14
+ def initialize(flags = Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT)
15
+ @flags = flags.to_i
15
16
  end
16
17
 
17
- def strict?; !@options.zero?; end
18
- def recover?; @options & RECOVER != 0; end
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
- status = Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s)
15
- raise Leptris::XML::Error,
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
- status = Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s)
22
- raise Leptris::XML::Error,
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 name/value
132
- # pairs: [name1, value1, name2, value2, ..., NULL]. Walk into 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
- return [] if attrs_ptr.null?
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 = str_ptr.null? ? "" : str_ptr.read_string
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
@@ -8,9 +8,8 @@ class Leptris::XML::Text < Leptris::XML::Node
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
- status = Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s)
12
- raise Leptris::XML::Error,
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.0.0
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: 1980-01-02 00:00:00.000000000 Z
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/c14n.rb
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: 4.0.16
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: []
@@ -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