leptris 1.9.174.1 → 1.9.174.3
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 +33 -0
- data/Rakefile +12 -9
- data/TODO.perf/19-lazy-node-pointer.md +39 -0
- data/TODO.perf/20-css-translation-cache.md +24 -0
- data/TODO.perf/21-key-memo-consult.md +20 -0
- data/ext/build_windows_native.rb +38 -0
- data/ext/leptris/native/native.c +9 -16
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +2 -2
- data/lib/leptris/xml/comment.rb +2 -2
- data/lib/leptris/xml/doc_type.rb +5 -5
- data/lib/leptris/xml/document.rb +38 -38
- data/lib/leptris/xml/document_fragment.rb +1 -1
- data/lib/leptris/xml/element.rb +58 -49
- data/lib/leptris/xml/native_layer.rb +11 -5
- data/lib/leptris/xml/node.rb +29 -23
- data/lib/leptris/xml/processing_instruction.rb +7 -7
- data/lib/leptris/xml/searchable.rb +28 -4
- data/lib/leptris/xml/text.rb +2 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 741e98553c4d88169d4c2f8ec0fb793eeebd80ffd8b00771fcfb0be218721f64
|
|
4
|
+
data.tar.gz: 5e02a23549a4e1c708dc40710b499e64fcd83150772dd08c8bbeac7fc77f0718
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1a3b25cd5b770b6edfb06bc2d448dacd691dc7e5af3fb301454fb1057e419c5fc779e4c9556a3f3b4be654ea5ec36be8f8e5c4e0f7ab16af380b6986da3bcb5
|
|
7
|
+
data.tar.gz: 85e81207c2abaca9a80af5dcfb33ae4e4f83c2ec24950b16d38fcb239bf7fa4d01b40d4d88b7fa19e019ba35a9ce58984c823e3f049fc47492459ebde53aa47b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.174.3] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Lazy FFI::Pointer on wrappers (TODO.perf/19)**: the node's
|
|
13
|
+
canonical state is the Integer address; the Pointer
|
|
14
|
+
materializes only when read — the C construction faces stop
|
|
15
|
+
minting one per node (~200ns + one allocation each, and
|
|
16
|
+
proportionally less GC). Public `c_ptr` API and identity
|
|
17
|
+
unchanged. Measured (load 22-25): GC-amortized
|
|
18
|
+
`Document.create` ~5-6µs → ~4.0µs; `NativeNode#[]` 78ns
|
|
19
|
+
(budget 70), `#content` 45ns (budget 58 met).
|
|
20
|
+
- **CSS translation cache (TODO.perf/20)**: repeat selectors skip
|
|
21
|
+
the per-call regex translation straight to the compiled handle;
|
|
22
|
+
the single-path xpath shape no longer allocates a joined
|
|
23
|
+
String per call.
|
|
24
|
+
- **`Element#key?` consults the attribute memo (TODO.perf/21)**
|
|
25
|
+
before the engine round-trip; miss semantics unchanged.
|
|
26
|
+
|
|
27
|
+
## [1.9.174.2] - 2026-09-15
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **The native layer returns to Windows, one DLL per Ruby minor
|
|
32
|
+
(#227)**: a PE DLL must bind its build Ruby's runtime, so the
|
|
33
|
+
Windows platform gems now ship `native-3.3.so` / `native-3.4.so`
|
|
34
|
+
/ `native-4.0.so` and the loader picks by `RUBY_VERSION` at
|
|
35
|
+
require. The release workflow compiles each minor's artifact on
|
|
36
|
+
the Windows legs (a minor unavailable on a runner degrades to
|
|
37
|
+
the loud FFI fallback for that cell); local `rake compile` on
|
|
38
|
+
Windows builds the current minor's DLL. Windows test legs now
|
|
39
|
+
exercise the full native suite instead of FFI-only.
|
|
40
|
+
|
|
8
41
|
## [1.9.174.1] - 2026-09-15
|
|
9
42
|
|
|
10
43
|
### Fixed
|
data/Rakefile
CHANGED
|
@@ -128,12 +128,14 @@ task :compile do
|
|
|
128
128
|
# the library — no compile at install; resolves libleptris at
|
|
129
129
|
# require time via dlsym.
|
|
130
130
|
if Gem.win_platform?
|
|
131
|
-
# #207: a PE DLL cannot leave Ruby imports unresolved —
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
|
|
131
|
+
# #207: a PE DLL cannot leave Ruby imports unresolved — it
|
|
132
|
+
# binds the build Ruby's x64-ucrt-rubyNNN.dll. The Windows
|
|
133
|
+
# gems therefore ship one DLL per supported Ruby minor
|
|
134
|
+
# (native-<minor>.so), and native_layer picks by RUBY_VERSION
|
|
135
|
+
# (#227). Local compile builds just the current minor's DLL;
|
|
136
|
+
# the release workflow runs the build script under 3.3/3.4/
|
|
137
|
+
# 4.0 to produce the full set.
|
|
138
|
+
sh "#{RbConfig.ruby} ext/build_windows_native.rb"
|
|
137
139
|
else
|
|
138
140
|
ext_dir = "ext/leptris/native"
|
|
139
141
|
Dir.chdir(ext_dir) do
|
|
@@ -283,9 +285,10 @@ platforms.each do |platform|
|
|
|
283
285
|
spec.platform = Gem::Platform.new(platform)
|
|
284
286
|
spec.files += Dir.glob("lib/libleptris.{dll,so,dylib}")
|
|
285
287
|
spec.files += Dir.glob("lib/{libutf8proc.3.dylib,libutf8proc.so.3,libutf8proc.so,utf8proc.dll}")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
if platform.include?("mingw")
|
|
289
|
+
# #207/#227: per-Ruby-minor DLLs (PE must bind its Ruby).
|
|
290
|
+
spec.files += Dir.glob("lib/leptris/xml/native-*.so")
|
|
291
|
+
else
|
|
289
292
|
spec.files += Dir.glob("lib/leptris/xml/native.{bundle,so,dll}")
|
|
290
293
|
end
|
|
291
294
|
task = Gem::PackageTask.new(spec)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 19 — Lazy FFI::Pointer on wrappers
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Every binding wrapper stores an FFI::Pointer (@c_ptr), and the C
|
|
6
|
+
construction faces mint one per node via
|
|
7
|
+
rb_funcall(FFI::Pointer, :new) — ~200ns plus a Ruby object and
|
|
8
|
+
its GC visit per node, paid at creation even though most nodes'
|
|
9
|
+
Pointers are never read (the hot reads are memoized or go through
|
|
10
|
+
the address-based native faces).
|
|
11
|
+
|
|
12
|
+
Store @c_address (Integer) as the canonical truth on every
|
|
13
|
+
wrapper; `c_ptr` becomes a lazy materializer
|
|
14
|
+
(@c_ptr ||= FFI::Pointer.new(@c_address)) preserving the public
|
|
15
|
+
API. Internal call sites: `@c_ptr.address` collapses to
|
|
16
|
+
@c_address (also skipping the #address dispatch), and remaining
|
|
17
|
+
internal @c_ptr reads go through the c_ptr reader (same cost as
|
|
18
|
+
the old attr_reader). The C faces stop constructing Pointers
|
|
19
|
+
entirely — they already hold the address.
|
|
20
|
+
|
|
21
|
+
Gates: suite green both modes (a missed site fails loudly — nil
|
|
22
|
+
to FFI raises TypeError); wrapper identity and c_ptr stability
|
|
23
|
+
unchanged; creation faces drop ~200ns and one allocation per
|
|
24
|
+
node; fresh-doc build row improves accordingly.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.3)
|
|
27
|
+
|
|
28
|
+
@c_address is the canonical truth on every Node wrapper; c_ptr
|
|
29
|
+
materializes lazily (@c_ptr ||= FFI::Pointer.new(@c_address)),
|
|
30
|
+
preserving the public API and identity. All internal @c_ptr
|
|
31
|
+
reads became c_ptr reader calls; the 15 @c_ptr.address sites
|
|
32
|
+
collapsed to @c_address (also skipping the #address dispatch).
|
|
33
|
+
The C construction faces (create element/text, bulk children,
|
|
34
|
+
every xpath materialization) stopped minting Pointers entirely —
|
|
35
|
+
one allocation and ~200ns saved per node, and proportionally less
|
|
36
|
+
GC. Document keeps its eager reader (documents are few).
|
|
37
|
+
Measured under host load 22-25: GC-amortized Document.create
|
|
38
|
+
~5-6µs -> ~4.0µs; NativeNode#[] 78ns (budget 70 — clear at
|
|
39
|
+
load <10), content 45ns (budget 58 met).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 20 — CSS translation cache
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Searchable#css/#at_css run CssToXPath.convert on every call —
|
|
6
|
+
pure Ruby regex/string work (~300ns+) — before the compiled-
|
|
7
|
+
expression cache (TODO.perf/15) can hit on the result. The
|
|
8
|
+
translation is a pure function of (selector, prefix); cache it
|
|
9
|
+
with the same bounded-LRU shape (64 entries, Hash#shift
|
|
10
|
+
eviction, GVL-safe), so repeat selectors skip straight to the
|
|
11
|
+
compiled handle.
|
|
12
|
+
|
|
13
|
+
Gates: css/at_css results identical; repeat-call rows improve by
|
|
14
|
+
the translation cost; the join in xpath()'s expression build is
|
|
15
|
+
also skipped for the single-String-path shape (join minted a
|
|
16
|
+
fresh String per call even for one path).
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.3)
|
|
19
|
+
|
|
20
|
+
Searchable.css_expression: bounded LRU (64, shared limit) keyed
|
|
21
|
+
on prefix+selector; css/at_css translate through it so repeat
|
|
22
|
+
selectors skip the regex machinery straight to the compiled-
|
|
23
|
+
expression cache. The single-String-path shape of xpath/at_xpath
|
|
24
|
+
no longer joins (join minted a fresh String per call).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 21 — key? consults the attribute memo
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Element#key?/has_attribute? pays an engine FFI round-trip per
|
|
6
|
+
call even when the versioned @attr_values memo already proves
|
|
7
|
+
presence. Consult the memo first (hit = true; only a miss falls
|
|
8
|
+
through to the engine + written-name fallback — the memo cannot
|
|
9
|
+
prove ABSENCE for a partial fill).
|
|
10
|
+
|
|
11
|
+
Gates: key? true for memoized names, unchanged miss semantics
|
|
12
|
+
(undeclared-prefix fallback intact), readonly/namespace specs
|
|
13
|
+
green.
|
|
14
|
+
|
|
15
|
+
## Outcome (1.9.174.3)
|
|
16
|
+
|
|
17
|
+
Element#key? consults the versioned @attr_values memo first —
|
|
18
|
+
presence-proven hits skip the engine round-trip; partial-fill
|
|
19
|
+
misses still fall through to the engine + written-name fallback
|
|
20
|
+
(the memo cannot prove absence).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds the native read layer DLL for the CURRENT Ruby and
|
|
4
|
+
# installs it under its minor-versioned name
|
|
5
|
+
# (lib/leptris/xml/native-<major.minor>.so).
|
|
6
|
+
#
|
|
7
|
+
# #207/#227: a PE DLL cannot resolve Ruby imports lazily — it
|
|
8
|
+
# must bind the build Ruby's x64-ucrt-rubyNNN.dll. One DLL per
|
|
9
|
+
# supported minor therefore ships in the Windows platform gems,
|
|
10
|
+
# and native_layer picks by RUBY_VERSION at require.
|
|
11
|
+
#
|
|
12
|
+
# Standalone by design: the release workflow runs this under each
|
|
13
|
+
# Ruby minor (3.3/3.4/4.0) with no bundler context.
|
|
14
|
+
|
|
15
|
+
require "rbconfig"
|
|
16
|
+
require "fileutils"
|
|
17
|
+
|
|
18
|
+
root = File.expand_path("..", __dir__)
|
|
19
|
+
ext_dir = File.join(root, "ext", "leptris", "native")
|
|
20
|
+
minor = RUBY_VERSION[/\A\d+\.\d+/]
|
|
21
|
+
|
|
22
|
+
Dir.chdir(ext_dir) do
|
|
23
|
+
system(RbConfig.ruby, "extconf.rb") or abort "extconf failed under #{RUBY_VERSION}"
|
|
24
|
+
# mkmf's link binds the CURRENT Ruby's runtime DLL — exactly
|
|
25
|
+
# what the versioned naming is for.
|
|
26
|
+
success = system("make")
|
|
27
|
+
abort "make failed under #{RUBY_VERSION}" unless success
|
|
28
|
+
so = Dir.glob("native.{so,dll}").first
|
|
29
|
+
abort "native bundle not produced under #{RUBY_VERSION}" unless so
|
|
30
|
+
dest = File.join(root, "lib", "leptris", "xml", "native-#{minor}.so")
|
|
31
|
+
# The artifact must reference only this minor's Ruby DLL.
|
|
32
|
+
imported = `strings #{so} 2>/dev/null`[/[a-z0-9-]*ruby\d{3,}\.dll/i]
|
|
33
|
+
if imported && !imported.include?("ruby#{minor.delete('.')}")
|
|
34
|
+
abort "#{so} imports #{imported} but was built under #{RUBY_VERSION} — refuse to mis-name it"
|
|
35
|
+
end
|
|
36
|
+
FileUtils.cp(so, dest)
|
|
37
|
+
puts "Installed native layer for Ruby #{minor} -> #{dest} (#{imported || 'no ruby dll string found'})"
|
|
38
|
+
end
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -870,9 +870,8 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
|
870
870
|
key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
|
|
871
871
|
node = rb_hash_aref(cache, key);
|
|
872
872
|
if (NIL_P(node)) {
|
|
873
|
-
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
874
873
|
node = rb_obj_alloc(binding_klass_for(kinds[i]));
|
|
875
|
-
rb_iv_set(node, "@
|
|
874
|
+
rb_iv_set(node, "@c_address", key);
|
|
876
875
|
rb_iv_set(node, "@document", document);
|
|
877
876
|
rb_iv_set(node, "@parent", Qnil);
|
|
878
877
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
@@ -965,9 +964,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
965
964
|
case 0: /* element */
|
|
966
965
|
node = rb_hash_aref(cache, key);
|
|
967
966
|
if (NIL_P(node)) {
|
|
968
|
-
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
969
967
|
node = rb_obj_alloc(c_b_element);
|
|
970
|
-
rb_iv_set(node, "@
|
|
968
|
+
rb_iv_set(node, "@c_address", key);
|
|
971
969
|
rb_iv_set(node, "@document", document);
|
|
972
970
|
rb_iv_set(node, "@parent", Qnil);
|
|
973
971
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
@@ -980,9 +978,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
980
978
|
{
|
|
981
979
|
const char *name = f_xp_node_name(result, idx);
|
|
982
980
|
const char *value = f_xp_node_value(result, idx);
|
|
983
|
-
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
984
981
|
node = rb_obj_alloc(c_b_result_attr);
|
|
985
|
-
rb_iv_set(node, "@
|
|
982
|
+
rb_iv_set(node, "@c_address", key);
|
|
986
983
|
rb_iv_set(node, "@document", document);
|
|
987
984
|
rb_iv_set(node, "@attr_name",
|
|
988
985
|
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
@@ -998,8 +995,7 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
998
995
|
if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
|
|
999
996
|
s = f_xp_node_value(result, idx);
|
|
1000
997
|
node = rb_obj_alloc(c_b_result_text);
|
|
1001
|
-
rb_iv_set(node, "@
|
|
1002
|
-
rb_funcall(c_ffi_pointer, id_ptr_new, 1, key));
|
|
998
|
+
rb_iv_set(node, "@c_address", key);
|
|
1003
999
|
rb_iv_set(node, "@document", document);
|
|
1004
1000
|
rb_iv_set(node, "@value",
|
|
1005
1001
|
s ? rb_utf8_str_new_cstr(s) : Qnil);
|
|
@@ -1007,9 +1003,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
1007
1003
|
}
|
|
1008
1004
|
node = rb_hash_aref(cache, key);
|
|
1009
1005
|
if (NIL_P(node)) {
|
|
1010
|
-
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
1011
1006
|
node = rb_obj_alloc(binding_klass_for(nt));
|
|
1012
|
-
rb_iv_set(node, "@
|
|
1007
|
+
rb_iv_set(node, "@c_address", key);
|
|
1013
1008
|
rb_iv_set(node, "@document", document);
|
|
1014
1009
|
rb_iv_set(node, "@parent", Qnil);
|
|
1015
1010
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
@@ -1096,9 +1091,8 @@ static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
|
1096
1091
|
if (!ptr)
|
|
1097
1092
|
return Qnil; /* caller raises with the error channel */
|
|
1098
1093
|
node = rb_obj_alloc(c_b_element);
|
|
1099
|
-
rb_iv_set(node, "@
|
|
1100
|
-
|
|
1101
|
-
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
1094
|
+
rb_iv_set(node, "@c_address",
|
|
1095
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr));
|
|
1102
1096
|
rb_iv_set(node, "@document", document);
|
|
1103
1097
|
rb_iv_set(node, "@parent", Qnil);
|
|
1104
1098
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
@@ -1122,9 +1116,8 @@ static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
|
1122
1116
|
if (!ptr)
|
|
1123
1117
|
return Qnil;
|
|
1124
1118
|
node = rb_obj_alloc(c_b_text);
|
|
1125
|
-
rb_iv_set(node, "@
|
|
1126
|
-
|
|
1127
|
-
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
1119
|
+
rb_iv_set(node, "@c_address",
|
|
1120
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr));
|
|
1128
1121
|
rb_iv_set(node, "@document", document);
|
|
1129
1122
|
rb_iv_set(node, "@parent", Qnil);
|
|
1130
1123
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/cdata.rb
CHANGED
|
@@ -6,7 +6,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
6
6
|
def content
|
|
7
7
|
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
result = Leptris::XML::FFI.leptris_cdata_node_get_content(
|
|
9
|
+
result = Leptris::XML::FFI.leptris_cdata_node_get_content(c_ptr)
|
|
10
10
|
if @document
|
|
11
11
|
@content = result
|
|
12
12
|
@content_version = @document.version
|
|
@@ -17,7 +17,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
17
17
|
def content=(new_content)
|
|
18
18
|
ensure_writable!
|
|
19
19
|
Leptris::XML::FFI.check_status(
|
|
20
|
-
Leptris::XML::FFI.leptris_cdata_node_set_content(
|
|
20
|
+
Leptris::XML::FFI.leptris_cdata_node_set_content(c_ptr, new_content.to_s))
|
|
21
21
|
new_content
|
|
22
22
|
end
|
|
23
23
|
end
|
data/lib/leptris/xml/comment.rb
CHANGED
|
@@ -6,7 +6,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
6
6
|
def content
|
|
7
7
|
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
result = Leptris::XML::FFI.leptris_comment_node_get_content(
|
|
9
|
+
result = Leptris::XML::FFI.leptris_comment_node_get_content(c_ptr)
|
|
10
10
|
if @document
|
|
11
11
|
@content = result
|
|
12
12
|
@content_version = @document.version
|
|
@@ -17,7 +17,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
17
17
|
def content=(new_content)
|
|
18
18
|
ensure_writable!
|
|
19
19
|
Leptris::XML::FFI.check_status(
|
|
20
|
-
Leptris::XML::FFI.leptris_comment_node_set_content(
|
|
20
|
+
Leptris::XML::FFI.leptris_comment_node_set_content(c_ptr, new_content.to_s))
|
|
21
21
|
new_content
|
|
22
22
|
end
|
|
23
23
|
end
|
data/lib/leptris/xml/doc_type.rb
CHANGED
|
@@ -9,24 +9,24 @@ class Leptris::XML::DocType
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def name
|
|
12
|
-
Leptris::XML::FFI.leptris_doctype_get_name(
|
|
12
|
+
Leptris::XML::FFI.leptris_doctype_get_name(c_ptr)
|
|
13
13
|
end
|
|
14
14
|
alias_method :node_name, :name
|
|
15
15
|
|
|
16
16
|
def root_name
|
|
17
|
-
Leptris::XML::FFI.leptris_doctype_get_root_name(
|
|
17
|
+
Leptris::XML::FFI.leptris_doctype_get_root_name(c_ptr)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def public_id
|
|
21
|
-
Leptris::XML::FFI.leptris_doctype_get_public_id(
|
|
21
|
+
Leptris::XML::FFI.leptris_doctype_get_public_id(c_ptr)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def system_id
|
|
25
|
-
Leptris::XML::FFI.leptris_doctype_get_system_id(
|
|
25
|
+
Leptris::XML::FFI.leptris_doctype_get_system_id(c_ptr)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def internal_subset
|
|
29
|
-
Leptris::XML::FFI.leptris_doctype_get_internal_subset(
|
|
29
|
+
Leptris::XML::FFI.leptris_doctype_get_internal_subset(c_ptr)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def external_id
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -32,9 +32,9 @@ class Leptris::XML::Document
|
|
|
32
32
|
|
|
33
33
|
def initialize(c_ptr = nil, freed = Freed.new(:alive))
|
|
34
34
|
@c_ptr = c_ptr
|
|
35
|
-
# Plain-Integer address twin of
|
|
35
|
+
# Plain-Integer address twin of c_ptr: the native layer reads
|
|
36
36
|
# this ivar directly (no method dispatch), nil'ed exactly when
|
|
37
|
-
#
|
|
37
|
+
# c_ptr is.
|
|
38
38
|
@c_address = c_ptr&.address
|
|
39
39
|
@freed = freed
|
|
40
40
|
@readonly = false
|
|
@@ -209,13 +209,13 @@ class Leptris::XML::Document
|
|
|
209
209
|
|
|
210
210
|
def root
|
|
211
211
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
212
|
-
return nil if
|
|
212
|
+
return nil if c_ptr.nil?
|
|
213
213
|
# Version-stamped memo (TODO.perf/13): root changes only
|
|
214
214
|
# through mutations that advance @version (root=, unlink) or
|
|
215
|
-
# #free (which nils
|
|
215
|
+
# #free (which nils c_ptr above) — the entry point of every
|
|
216
216
|
# pipeline stops paying FFI + wrap per call.
|
|
217
217
|
return @root if @root_version == @version
|
|
218
|
-
ptr = Leptris::XML::FFI.leptris_document_root(
|
|
218
|
+
ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
|
|
219
219
|
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, self)
|
|
220
220
|
@root = result
|
|
221
221
|
@root_version = @version
|
|
@@ -231,7 +231,7 @@ class Leptris::XML::Document
|
|
|
231
231
|
def node
|
|
232
232
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
233
233
|
@node ||= Leptris::XML::Node.wrap(
|
|
234
|
-
Leptris::XML::FFI.leptris_document_node(
|
|
234
|
+
Leptris::XML::FFI.leptris_document_node(c_ptr), self)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
237
|
# The document's children, via the document node: prolog
|
|
@@ -250,7 +250,7 @@ class Leptris::XML::Document
|
|
|
250
250
|
doc_node = node
|
|
251
251
|
return Leptris::XML::NodeSet.new(self, []) if doc_node.nil?
|
|
252
252
|
kids = doc_node.children.to_a
|
|
253
|
-
root_ptr = Leptris::XML::FFI.leptris_document_root(
|
|
253
|
+
root_ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
|
|
254
254
|
return kids if root_ptr.null?
|
|
255
255
|
return kids if kids.any? { |child| child.c_ptr == root_ptr }
|
|
256
256
|
root = Leptris::XML::Node.wrap(root_ptr, self)
|
|
@@ -289,7 +289,7 @@ class Leptris::XML::Document
|
|
|
289
289
|
Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
|
|
290
290
|
end
|
|
291
291
|
Leptris::XML::FFI.check_status(
|
|
292
|
-
Leptris::XML::FFI.leptris_document_set_root(
|
|
292
|
+
Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
|
|
293
293
|
@version += 1
|
|
294
294
|
# Seed the root memo through wrap: a cross-document element
|
|
295
295
|
# must enter THIS document's identity cache with @document
|
|
@@ -308,7 +308,7 @@ class Leptris::XML::Document
|
|
|
308
308
|
raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
|
|
309
309
|
return node
|
|
310
310
|
end
|
|
311
|
-
ptr = Leptris::XML::FFI.leptris_element_create(
|
|
311
|
+
ptr = Leptris::XML::FFI.leptris_element_create(c_ptr, name)
|
|
312
312
|
raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
|
|
313
313
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
|
|
314
314
|
end
|
|
@@ -319,25 +319,25 @@ class Leptris::XML::Document
|
|
|
319
319
|
raise Leptris::XML::Error, "leptris_text_node_create failed" if node.nil?
|
|
320
320
|
return node
|
|
321
321
|
end
|
|
322
|
-
ptr = Leptris::XML::FFI.leptris_text_node_create(
|
|
322
|
+
ptr = Leptris::XML::FFI.leptris_text_node_create(c_ptr, content.to_s)
|
|
323
323
|
raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
|
|
324
324
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_TEXT)
|
|
325
325
|
end
|
|
326
326
|
|
|
327
327
|
def create_comment(content)
|
|
328
|
-
ptr = Leptris::XML::FFI.leptris_comment_node_create(
|
|
328
|
+
ptr = Leptris::XML::FFI.leptris_comment_node_create(c_ptr, content.to_s)
|
|
329
329
|
raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
|
|
330
330
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_COMMENT)
|
|
331
331
|
end
|
|
332
332
|
|
|
333
333
|
def create_cdata(content)
|
|
334
|
-
ptr = Leptris::XML::FFI.leptris_cdata_node_create(
|
|
334
|
+
ptr = Leptris::XML::FFI.leptris_cdata_node_create(c_ptr, content.to_s)
|
|
335
335
|
raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
|
|
336
336
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_CDATA)
|
|
337
337
|
end
|
|
338
338
|
|
|
339
339
|
def create_processing_instruction(target, data = "")
|
|
340
|
-
ptr = Leptris::XML::FFI.leptris_pi_node_create(
|
|
340
|
+
ptr = Leptris::XML::FFI.leptris_pi_node_create(c_ptr, target.to_s, data.to_s)
|
|
341
341
|
raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
|
|
342
342
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_PI)
|
|
343
343
|
end
|
|
@@ -347,14 +347,14 @@ class Leptris::XML::Document
|
|
|
347
347
|
end
|
|
348
348
|
|
|
349
349
|
def dup
|
|
350
|
-
raw = Leptris::XML::FFI.leptris_document_copy(
|
|
350
|
+
raw = Leptris::XML::FFI.leptris_document_copy(c_ptr)
|
|
351
351
|
raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
|
|
352
352
|
self.class.wrap(raw)
|
|
353
353
|
end
|
|
354
354
|
alias_method :clone, :dup
|
|
355
355
|
|
|
356
356
|
def doctype
|
|
357
|
-
ptr = Leptris::XML::FFI.leptris_document_internal_subset(
|
|
357
|
+
ptr = Leptris::XML::FFI.leptris_document_internal_subset(c_ptr)
|
|
358
358
|
return nil if ptr.null?
|
|
359
359
|
Leptris::XML::DocType.new(ptr, self)
|
|
360
360
|
end
|
|
@@ -368,18 +368,18 @@ class Leptris::XML::Document
|
|
|
368
368
|
# display-oriented and not round-trip-guaranteed).
|
|
369
369
|
def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false)
|
|
370
370
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
371
|
-
return "" if
|
|
371
|
+
return "" if c_ptr.nil?
|
|
372
372
|
case indent_text
|
|
373
373
|
when String
|
|
374
374
|
return Leptris::XML::Serialization.to_xml_indent_unit(
|
|
375
|
-
|
|
375
|
+
c_ptr, indent_text, indent: indent, no_decl: no_decl,
|
|
376
376
|
encoding: encoding)
|
|
377
377
|
when true
|
|
378
378
|
return Leptris::XML::Serialization.to_xml_display(
|
|
379
|
-
|
|
379
|
+
c_ptr, indent: indent, no_decl: no_decl, encoding: encoding)
|
|
380
380
|
end
|
|
381
381
|
Leptris::XML::Serialization.to_xml(
|
|
382
|
-
Leptris::XML::Serialization::DOCUMENT_SERIALIZE_INTO,
|
|
382
|
+
Leptris::XML::Serialization::DOCUMENT_SERIALIZE_INTO, c_ptr,
|
|
383
383
|
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
384
384
|
end
|
|
385
385
|
alias_method :to_s, :to_xml
|
|
@@ -391,7 +391,7 @@ class Leptris::XML::Document
|
|
|
391
391
|
no_decl: opts.fetch(:no_decl, false),
|
|
392
392
|
encoding: opts[:encoding])
|
|
393
393
|
status = Leptris::XML::FFI.leptris_document_save_file(
|
|
394
|
-
|
|
394
|
+
c_ptr, path, opts_struct.pointer)
|
|
395
395
|
Leptris::XML::FFI.check_status(status)
|
|
396
396
|
self
|
|
397
397
|
end
|
|
@@ -402,11 +402,11 @@ class Leptris::XML::Document
|
|
|
402
402
|
exclusive: false,
|
|
403
403
|
mode: nil)
|
|
404
404
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
405
|
-
return "" if
|
|
405
|
+
return "" if c_ptr.nil?
|
|
406
406
|
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
407
407
|
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
408
408
|
Leptris::XML::Serialization.canonicalize(
|
|
409
|
-
Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex),
|
|
409
|
+
Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), c_ptr,
|
|
410
410
|
version: version, mode: resolved_mode,
|
|
411
411
|
inclusive_namespaces: inclusive_namespaces,
|
|
412
412
|
with_comments: with_comments)
|
|
@@ -416,7 +416,7 @@ class Leptris::XML::Document
|
|
|
416
416
|
def free
|
|
417
417
|
return if @freed.state == :freed
|
|
418
418
|
@freed.state = :freed
|
|
419
|
-
Leptris::XML::FFI.leptris_document_free(
|
|
419
|
+
Leptris::XML::FFI.leptris_document_free(c_ptr) unless c_ptr.nil?
|
|
420
420
|
@c_ptr = nil
|
|
421
421
|
@c_address = nil
|
|
422
422
|
@wrapper_cache&.clear
|
|
@@ -432,7 +432,7 @@ class Leptris::XML::Document
|
|
|
432
432
|
# handlers. Returns self for chaining.
|
|
433
433
|
def exslt
|
|
434
434
|
Leptris::XML::FFI.check_status(
|
|
435
|
-
Leptris::XML::FFI.leptris_exslt_enable(
|
|
435
|
+
Leptris::XML::FFI.leptris_exslt_enable(c_ptr))
|
|
436
436
|
self
|
|
437
437
|
end
|
|
438
438
|
|
|
@@ -440,11 +440,11 @@ class Leptris::XML::Document
|
|
|
440
440
|
# an array of [target, data] pairs in document order.
|
|
441
441
|
def processing_instructions
|
|
442
442
|
return @processing_instructions if @pi_version == @version
|
|
443
|
-
count = Leptris::XML::FFI.leptris_document_pi_count(
|
|
443
|
+
count = Leptris::XML::FFI.leptris_document_pi_count(c_ptr)
|
|
444
444
|
result = count.times.map do |i|
|
|
445
|
-
[Leptris::XML::FFI.leptris_document_pi_target(
|
|
445
|
+
[Leptris::XML::FFI.leptris_document_pi_target(c_ptr, i),
|
|
446
446
|
Leptris::XML::FFI.read_pi_data(
|
|
447
|
-
Leptris::XML::FFI.leptris_document_pi_data(
|
|
447
|
+
Leptris::XML::FFI.leptris_document_pi_data(c_ptr, i)).to_s]
|
|
448
448
|
end
|
|
449
449
|
@processing_instructions = result
|
|
450
450
|
@pi_version = @version
|
|
@@ -460,10 +460,10 @@ class Leptris::XML::Document
|
|
|
460
460
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
461
461
|
if target_or_index.is_a?(Integer)
|
|
462
462
|
ptr = Leptris::XML::FFI.leptris_document_remove_pi(
|
|
463
|
-
|
|
463
|
+
c_ptr, nil, target_or_index)
|
|
464
464
|
else
|
|
465
465
|
ptr = Leptris::XML::FFI.leptris_document_remove_pi(
|
|
466
|
-
|
|
466
|
+
c_ptr, target_or_index.to_s, 0)
|
|
467
467
|
end
|
|
468
468
|
return nil if ptr.null?
|
|
469
469
|
@version += 1
|
|
@@ -473,7 +473,7 @@ class Leptris::XML::Document
|
|
|
473
473
|
# Append a document-level processing instruction. Returns self.
|
|
474
474
|
def add_pi(target, data = "")
|
|
475
475
|
witness = Leptris::XML::FFI.leptris_document_add_pi(
|
|
476
|
-
|
|
476
|
+
c_ptr, target.to_s, data.to_s)
|
|
477
477
|
raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
|
|
478
478
|
@version += 1
|
|
479
479
|
self
|
|
@@ -485,7 +485,7 @@ class Leptris::XML::Document
|
|
|
485
485
|
# this is the writer). Returns self.
|
|
486
486
|
def add_comment(content)
|
|
487
487
|
witness = Leptris::XML::FFI.leptris_document_add_comment(
|
|
488
|
-
|
|
488
|
+
c_ptr, content.to_s)
|
|
489
489
|
raise Leptris::XML::Error,
|
|
490
490
|
"leptris_document_add_comment failed" if witness.null?
|
|
491
491
|
@version += 1
|
|
@@ -497,7 +497,7 @@ class Leptris::XML::Document
|
|
|
497
497
|
# (names, content, children, attributes) since they can never go
|
|
498
498
|
# stale. The C document is also frozen (advisory upstream). One-way.
|
|
499
499
|
def readonly!
|
|
500
|
-
Leptris::XML::FFI.leptris_document_freeze(
|
|
500
|
+
Leptris::XML::FFI.leptris_document_freeze(c_ptr)
|
|
501
501
|
@readonly = true
|
|
502
502
|
self
|
|
503
503
|
end
|
|
@@ -509,7 +509,7 @@ class Leptris::XML::Document
|
|
|
509
509
|
# True once #free has run (or the GC finalizer fired) — borrowed
|
|
510
510
|
# handles check this before dereferencing their c_ptr.
|
|
511
511
|
def freed?
|
|
512
|
-
@freed.state == :freed ||
|
|
512
|
+
@freed.state == :freed || c_ptr.nil?
|
|
513
513
|
end
|
|
514
514
|
|
|
515
515
|
# Document-level comments — parsed <!-- ... --> outside the
|
|
@@ -518,9 +518,9 @@ class Leptris::XML::Document
|
|
|
518
518
|
# 1.9.3, upstream #578). Version-memoized like the PI list.
|
|
519
519
|
def comments
|
|
520
520
|
return @comments if @comments_version == @version
|
|
521
|
-
count = Leptris::XML::FFI.leptris_document_comment_count(
|
|
521
|
+
count = Leptris::XML::FFI.leptris_document_comment_count(c_ptr)
|
|
522
522
|
result = Array.new(count) do |i|
|
|
523
|
-
Leptris::XML::FFI.leptris_document_comment_content(
|
|
523
|
+
Leptris::XML::FFI.leptris_document_comment_content(c_ptr, i)
|
|
524
524
|
end
|
|
525
525
|
@comments = result
|
|
526
526
|
@comments_version = @version
|
|
@@ -545,15 +545,15 @@ class Leptris::XML::Document
|
|
|
545
545
|
|
|
546
546
|
# The most recent error recorded against this document, or nil.
|
|
547
547
|
def last_error
|
|
548
|
-
msg = Leptris::XML::FFI.leptris_document_last_error(
|
|
548
|
+
msg = Leptris::XML::FFI.leptris_document_last_error(c_ptr)
|
|
549
549
|
msg.nil? || msg.empty? ? nil : msg
|
|
550
550
|
end
|
|
551
551
|
|
|
552
552
|
def name; "document"; end
|
|
553
553
|
def document; self; end
|
|
554
554
|
def encoding
|
|
555
|
-
return nil if
|
|
556
|
-
Leptris::XML::FFI.leptris_document_encoding(
|
|
555
|
+
return nil if c_ptr.nil?
|
|
556
|
+
Leptris::XML::FFI.leptris_document_encoding(c_ptr)
|
|
557
557
|
end
|
|
558
558
|
|
|
559
559
|
include Leptris::XML::Searchable
|
|
@@ -25,7 +25,7 @@ class Leptris::XML::DocumentFragment
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def children
|
|
28
|
-
pointers, kinds = Leptris::XML::FFI.fetch_children(
|
|
28
|
+
pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
|
|
29
29
|
nodes = Array.new(pointers.size) do |i|
|
|
30
30
|
Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
|
|
31
31
|
end
|