leptris 1.9.201.4 → 1.9.207.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/Rakefile +8 -2
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +25 -0
- data/lib/leptris/xml/dtd.rb +113 -0
- data/lib/leptris/xml/ffi.rb +75 -0
- data/lib/leptris/xml.rb +1 -0
- data/vendor-src/libleptris/CHANGELOG.md +79 -0
- data/vendor-src/libleptris/CLAUDE.md +1 -1
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/README.adoc +26 -0
- data/vendor-src/libleptris/TODO.max-perf/2-3-oneshot-tokenizer-design.md +98 -0
- data/vendor-src/libleptris/cli/commands/validate.c +4 -2
- data/vendor-src/libleptris/docs/guide/parsing.md +30 -0
- data/vendor-src/libleptris/scripts/check_export_surface.py +20 -0
- data/vendor-src/libleptris/src/CMakeLists.txt +44 -1
- data/vendor-src/libleptris/src/include/leptris/descriptor.h +7 -1
- data/vendor-src/libleptris/src/include/leptris/dom/document.h +7 -1
- data/vendor-src/libleptris/src/include/leptris/dom/element.h +7 -1
- data/vendor-src/libleptris/src/include/leptris/dom/serialize.h +7 -1
- data/vendor-src/libleptris/src/include/leptris/dtd.h +46 -9
- data/vendor-src/libleptris/src/include/leptris/error.h +63 -1
- data/vendor-src/libleptris/src/include/leptris/html.h +79 -0
- data/vendor-src/libleptris/src/include/leptris/sax/sax.h +20 -1
- data/vendor-src/libleptris/src/include/leptris/types.h +29 -1
- data/vendor-src/libleptris/src/include/leptris/xpath/xpath.h +7 -1
- data/vendor-src/libleptris/src/include/leptris.h +59 -37
- data/vendor-src/libleptris/src/leptris/core.c +28 -0
- data/vendor-src/libleptris/src/leptris/dom/diag.h +2 -13
- data/vendor-src/libleptris/src/leptris/dom/digest.c +5 -1
- data/vendor-src/libleptris/src/leptris/dom/doctype.c +27 -0
- data/vendor-src/libleptris/src/leptris/dom/element.c +54 -9
- data/vendor-src/libleptris/src/leptris/dom/element.h +5 -0
- data/vendor-src/libleptris/src/leptris/dom/element_modify.c +46 -6
- data/vendor-src/libleptris/src/leptris/dtd/model.h +6 -0
- data/vendor-src/libleptris/src/leptris/dtd/parser.c +49 -12
- data/vendor-src/libleptris/src/leptris/error.c +22 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +61 -6
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +156 -42
- data/vendor-src/libleptris/src/leptris/leptris.c +11 -2
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +23 -1
- data/vendor-src/libleptris/src/leptris/sax/pull.c +62 -18
- data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +7 -0
- data/vendor-src/libleptris/src/leptris/sax/streaming.c +155 -4
- data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +19 -2
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +126 -0
- data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +653 -151
- data/vendor-src/libleptris/src/leptris/xpath/parser.c +4 -2
- data/vendor-src/libleptris/src/leptris/xslt/xslt_functions.c +9 -5
- data/vendor-src/libleptris/test/dom/test_digest.cpp +36 -0
- data/vendor-src/libleptris/test/dom/test_dom.cpp +24 -0
- data/vendor-src/libleptris/test/dom/test_node_surface_parity.cpp +81 -0
- data/vendor-src/libleptris/test/dtd/test_dtd_validate.cpp +113 -0
- data/vendor-src/libleptris/test/html/test_html.cpp +51 -0
- data/vendor-src/libleptris/test/parser/test_parser.cpp +43 -0
- data/vendor-src/libleptris/test/sax/test_pull.cpp +32 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/replace.xml +865 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/string-join.xml +449 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/tokenize.xml +639 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +74 -6
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b10f689b2142684316157264cfd935433d6d0d22db8cf90b4a27e9c4ba79bd88
|
|
4
|
+
data.tar.gz: f3e625c375d995087bec8cd6c058a7a9c900791125f1422f6da8b2436e90047b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddf28875bb34e2eb14074c51834614f5cb5acddf0a33c58f55a50d9e6970a065eb70cb6f3da51c1114cad3a8c062a6dec98d6ce271ff27a81a77033f92e774ea
|
|
7
|
+
data.tar.gz: b1c7bc0629b2840a7a23d71bd27b08303426ab7b013d8a272d9a1fedc82b8f40ba8fdd6805724f878fbf4374752912658ea5922e79548f16a53a859cf4de3164
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,75 @@ 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.204.0] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
### Added — document-level mutation completed: #1229 (the 1.9.204 surface)
|
|
11
|
+
|
|
12
|
+
- **`Document#clear_declaration`** — un-sets the XML declaration
|
|
13
|
+
(clears version/encoding and the standalone marker); the
|
|
14
|
+
document serializes exactly as if the input had none.
|
|
15
|
+
Idempotent; returns self.
|
|
16
|
+
- **`Document#remove_doctype`** — un-sets the DOCTYPE; it leaves
|
|
17
|
+
the child chain and the serializer's view while the DocType
|
|
18
|
+
stays pool-owned and readable (`doc.doctype.name` et al.) until
|
|
19
|
+
`#free`. Returns true when removed, false when the document had
|
|
20
|
+
none. Rounds out the write-half next to `#set_doctype` /
|
|
21
|
+
`doc.doctype` (the read/create faces wired earlier).
|
|
22
|
+
- Completes upstream leptris/leptris#1229 and, with it, the
|
|
23
|
+
leptris-ruby#275 umbrella (PI removal 1.9.9, declaration clear +
|
|
24
|
+
DOCTYPE unset 1.9.204). Spec battery in
|
|
25
|
+
`document_construction_spec.rb`.
|
|
26
|
+
- Engine adoption: 1.9.203 (`leptris_dtd_parse` honors the
|
|
27
|
+
NULL-on-error contract, upstream #1211) and 1.9.204 (html.h
|
|
28
|
+
subsystem header, one-shot name fast paths, one-call buffer-mode
|
|
29
|
+
pull feeds). #1229's two exports are wired as the
|
|
30
|
+
`Document#clear_declaration` / `#remove_doctype` faces; the
|
|
31
|
+
other two 1.9.204 exports attach for lockstep parity
|
|
32
|
+
(`leptris_sax_parser_set_one_shot` — the binding's one-shot SAX
|
|
33
|
+
entry is one-shot by construction and pull's memory source sets
|
|
34
|
+
it engine-side; `leptris_alloc_buffer` — engine-heap allocation
|
|
35
|
+
for callback-owned buffers like the DTD PE loader, released via
|
|
36
|
+
the already-bound `leptris_free_string`). Audit 333/333.
|
|
37
|
+
|
|
38
|
+
### Fixed — portability (audit:symbols + POWER vendor mapping)
|
|
39
|
+
|
|
40
|
+
- `nm -gU` → `nm -g`: GNU binutils nm (Debian bookworm 2.40)
|
|
41
|
+
accepts `-U` but then ignores the file operand entirely
|
|
42
|
+
(`nm: 'a.out': No such file`, empty export list, guaranteed
|
|
43
|
+
false drift). Undefined externals print two columns, so the
|
|
44
|
+
existing `split[2]` + `compact` already skips them on every nm
|
|
45
|
+
flavor. The engine build itself succeeded on ppc64le this round
|
|
46
|
+
— #1196's LTO crash did not reproduce.
|
|
47
|
+
- `vendor_platforms_for` normalizes RbConfig's `powerpc64le` to
|
|
48
|
+
the gem-tier spelling `ppc64le`: the JVM-host rebuild ("java" →
|
|
49
|
+
host cpu-os) matched no tier on POWER hosts, so JRuby/
|
|
50
|
+
TruffleRuby there missed their vendored binary. Surfaced by the
|
|
51
|
+
ppc64le qemu leg (ffi_spec:223) once the audit fix let the suite
|
|
52
|
+
run; the spec now asserts JVM ≡ host tier.
|
|
53
|
+
|
|
54
|
+
### Not fixed despite upstream closes
|
|
55
|
+
|
|
56
|
+
- **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
|
|
57
|
+
## [1.9.207.0] - 2026-09-19
|
|
58
|
+
|
|
59
|
+
### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
|
|
60
|
+
|
|
61
|
+
- Vendored libleptris 1.9.207: the close-tag fast-path compare
|
|
62
|
+
masked the wrong end of its 8-byte name load on big-endian hosts,
|
|
63
|
+
rejecting ordinary paired-tag documents with "malformed input"
|
|
64
|
+
(434/727 binding-suite failures under qemu s390x). The mask now
|
|
65
|
+
follows memory order per byte dialect; the s390x-linux-musl
|
|
66
|
+
platform gem becomes viable with this release.
|
|
67
|
+
|
|
68
|
+
### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
|
|
69
|
+
|
|
70
|
+
- audit:symbols lockstep attachments: recover diagnostics
|
|
71
|
+
(leptris_document_parse_diag[_count]), the standalone DTD family
|
|
72
|
+
(leptris_dtd_*), declaration/doctype removal, engine-heap buffer
|
|
73
|
+
allocation, and one-shot SAX. leptris_set_error stays internal —
|
|
74
|
+
the engine now hides it on ELF too (leptris#1243), so the export
|
|
75
|
+
surface agrees across Linux/macOS/Windows.
|
|
76
|
+
|
|
8
77
|
## [1.9.201.3] - 2026-09-19
|
|
9
78
|
|
|
10
79
|
### Added — the HTML facade (Nokogiri-shaped entry points)
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
9
|
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
10
|
# CHANGELOG when libleptris releases.
|
|
11
|
-
LIBLEPTRIS_VERSION = "1.9.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.207"
|
|
12
12
|
# Vendored alongside libleptris for fn:normalize-unicode (TODO
|
|
13
13
|
# .restructure/20): built per platform with a RELOCATABLE @rpath
|
|
14
14
|
# install name, loaded by ffi.rb before libleptris so the
|
|
@@ -326,7 +326,13 @@ namespace :audit do
|
|
|
326
326
|
unless lib
|
|
327
327
|
abort "audit:symbols: vendored library not found — run rake compile"
|
|
328
328
|
end
|
|
329
|
-
|
|
329
|
+
# Plain -g, not -U: GNU binutils nm (Debian bookworm 2.40)
|
|
330
|
+
# accepts -U but then ignores the file operand entirely
|
|
331
|
+
# ("nm: 'a.out': No such file", empty export list, guaranteed
|
|
332
|
+
# false drift — the ppc64le leg). Undefined externals print two
|
|
333
|
+
# columns (type, name), so split[2] is nil for them and the
|
|
334
|
+
# compact below already drops them on every nm flavor.
|
|
335
|
+
exported = `nm -g #{lib}`
|
|
330
336
|
.lines.map { |l| l.split[2] }.compact
|
|
331
337
|
.map { |n| n.sub(/\A_/, "") }
|
|
332
338
|
.select { |n| n.start_with?("leptris_") }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -635,6 +635,31 @@ class Leptris::XML::Document
|
|
|
635
635
|
Leptris::XML::DocType.new(dt, self)
|
|
636
636
|
end
|
|
637
637
|
|
|
638
|
+
# Un-set the XML declaration (libleptris 1.9.204, upstream
|
|
639
|
+
# #1229): the document stops being declaration-bearing and
|
|
640
|
+
# serialization emits none — exactly as if the input had none.
|
|
641
|
+
# Clears version/encoding and the standalone marker. Idempotent.
|
|
642
|
+
# Returns self.
|
|
643
|
+
def clear_declaration
|
|
644
|
+
Leptris::XML::FFI.check_status(
|
|
645
|
+
Leptris::XML::FFI.leptris_document_clear_declaration(c_ptr))
|
|
646
|
+
@version += 1
|
|
647
|
+
self
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Un-set the document's DOCTYPE (libleptris 1.9.204, upstream
|
|
651
|
+
# #1229). The DocType stays pool-owned and readable via #doctype
|
|
652
|
+
# until #free; it leaves the child chain and the serializer's
|
|
653
|
+
# view. Returns true when removed, false when the document had
|
|
654
|
+
# none.
|
|
655
|
+
def remove_doctype
|
|
656
|
+
rc = Leptris::XML::FFI.leptris_document_remove_doctype(c_ptr)
|
|
657
|
+
return false if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND
|
|
658
|
+
Leptris::XML::FFI.check_status(rc)
|
|
659
|
+
@version += 1
|
|
660
|
+
true
|
|
661
|
+
end
|
|
662
|
+
|
|
638
663
|
# Append a document-level comment (epilog position, after the
|
|
639
664
|
# root element) — the add_pi twin (libleptris 1.9.160, #1032;
|
|
640
665
|
# parsed document-level comments round-tripped since 1.9.3/#578,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Leptris::XML
|
|
6
|
+
# DTD validation (the libleptris 1.9.202-exported surface, the
|
|
7
|
+
# leptris validate --dtd CLI's library face): parse an internal
|
|
8
|
+
# subset, optionally merge an external subset (the application
|
|
9
|
+
# owns I/O — read the resource named by the DOCTYPE system id
|
|
10
|
+
# and hand the bytes over), then validate any number of documents.
|
|
11
|
+
# The RelaxNG shape: parse once, validate many; errors carry
|
|
12
|
+
# message/element/line/column.
|
|
13
|
+
#
|
|
14
|
+
# dtd = Leptris::XML::DTD.parse(<<~DTD)
|
|
15
|
+
# <!ELEMENT book (title, author+)>
|
|
16
|
+
# <!ATTLIST book id ID #REQUIRED>
|
|
17
|
+
# DTD
|
|
18
|
+
# dtd.valid?(doc)
|
|
19
|
+
# dtd.validate(doc) # => [{message:, element:, line:, column:}]
|
|
20
|
+
#
|
|
21
|
+
module DTD
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def parse(dtd_content)
|
|
25
|
+
DTDHandle.parse(dtd_content)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The document's own internal subset (from its DOCTYPE), as a
|
|
29
|
+
# validatable handle: document-owned, never freed by the caller.
|
|
30
|
+
# Extend it with an external subset via #merge_external_subset.
|
|
31
|
+
def from_document(document)
|
|
32
|
+
DTDHandle.wrap_owned(
|
|
33
|
+
Leptris::XML::FFI.leptris_document_get_dtd(document.c_ptr))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# One struct-free convenience: validate +first_error. Returns
|
|
37
|
+
# nil when valid.
|
|
38
|
+
def first_error(document, dtd_content)
|
|
39
|
+
DTDHandle.parse(dtd_content).first_error(document)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class DTDHandle
|
|
43
|
+
def self.parse(dtd_content)
|
|
44
|
+
content = dtd_content.to_s
|
|
45
|
+
raw = Leptris::XML::FFI.leptris_dtd_parse(
|
|
46
|
+
content, content.bytesize)
|
|
47
|
+
if raw.null?
|
|
48
|
+
raise Leptris::XML::Error,
|
|
49
|
+
"DTD parse failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
50
|
+
end
|
|
51
|
+
new(OwnedHandle.new(raw))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.wrap_owned(ptr)
|
|
55
|
+
new(nil, ptr)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class OwnedHandle < ::FFI::AutoPointer
|
|
59
|
+
def self.release(ptr)
|
|
60
|
+
Leptris::XML::FFI.leptris_dtd_free(ptr)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(owned, doc_owned_ptr = nil)
|
|
65
|
+
@owned = owned
|
|
66
|
+
@ptr = doc_owned_ptr || owned
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def c_ptr
|
|
70
|
+
@ptr
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Merge an external subset (the application reads the resource;
|
|
74
|
+
# the library never does I/O). First declaration of a name wins,
|
|
75
|
+
# so the internal subset is never overridden.
|
|
76
|
+
def merge_external_subset(content)
|
|
77
|
+
c = content.to_s
|
|
78
|
+
rc = Leptris::XML::FFI.leptris_dtd_parse_external_subset(
|
|
79
|
+
c_ptr, c, c.bytesize)
|
|
80
|
+
raise Leptris::XML::Error, "external subset merge failed" if rc.negative?
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def valid?(document)
|
|
85
|
+
validate(document).empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# [{ message:, element:, line:, column: }] — empty when valid.
|
|
89
|
+
def validate(document)
|
|
90
|
+
err = Leptris::XML::FFI::DTDErrorStruct.new
|
|
91
|
+
rc = Leptris::XML::FFI.leptris_dtd_validate(
|
|
92
|
+
document.c_ptr, c_ptr, err)
|
|
93
|
+
return [] if rc == 1
|
|
94
|
+
return [{ message: "internal validation error" }] if rc.negative?
|
|
95
|
+
message = err[:message]
|
|
96
|
+
return [{ message: "validation failed" }] if message.nil?
|
|
97
|
+
[{
|
|
98
|
+
message: message,
|
|
99
|
+
element: err[:element_name],
|
|
100
|
+
line: err[:line],
|
|
101
|
+
column: err[:column],
|
|
102
|
+
}]
|
|
103
|
+
ensure
|
|
104
|
+
Leptris::XML::FFI.leptris_dtd_error_free(err) unless rc == 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# nil when valid — the single-error convenience.
|
|
108
|
+
def first_error(document)
|
|
109
|
+
validate(document).first
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -26,6 +26,9 @@ module Leptris
|
|
|
26
26
|
end
|
|
27
27
|
cpu = RbConfig::CONFIG["host_cpu"].to_s
|
|
28
28
|
cpu = "aarch64" if cpu.empty?
|
|
29
|
+
# RbConfig says "powerpc64le"; the gem/vendor tier is
|
|
30
|
+
# "ppc64le-linux" (the gem platform spelling).
|
|
31
|
+
cpu = "ppc64le" if cpu == "powerpc64le"
|
|
29
32
|
os = host_os =~ /darwin/ ? "darwin" : "linux"
|
|
30
33
|
ruby_platform = "#{cpu}-#{os}"
|
|
31
34
|
end
|
|
@@ -118,6 +121,7 @@ module Leptris
|
|
|
118
121
|
typedef :pointer, :leptris_node_ref
|
|
119
122
|
typedef :pointer, :leptris_attribute
|
|
120
123
|
typedef :pointer, :leptris_doctype
|
|
124
|
+
typedef :pointer, :leptris_dtd
|
|
121
125
|
typedef :pointer, :leptris_xpath_result
|
|
122
126
|
typedef :pointer, :leptris_xpath_ns_set
|
|
123
127
|
typedef :pointer, :leptris_xpath_compiled
|
|
@@ -133,6 +137,7 @@ module Leptris
|
|
|
133
137
|
typedef :pointer, :leptris_iterparse
|
|
134
138
|
typedef :pointer, :leptris_xpath_var_set
|
|
135
139
|
typedef :pointer, :leptris_sax_parser
|
|
140
|
+
typedef :pointer, :leptris_dtd
|
|
136
141
|
typedef :int, :leptris_status
|
|
137
142
|
|
|
138
143
|
class SAXHandler < ::FFI::Struct
|
|
@@ -381,6 +386,12 @@ attach_function :leptris_parse_string,
|
|
|
381
386
|
[:leptris_document, :int], :leptris_status
|
|
382
387
|
attach_function :leptris_document_set_doctype,
|
|
383
388
|
[:leptris_document, :string, :string, :string], :leptris_doctype
|
|
389
|
+
# The #1229 remove-half (libleptris 1.9.204): un-set the
|
|
390
|
+
# declaration; un-set the DOCTYPE (NOT_FOUND when none set).
|
|
391
|
+
attach_function :leptris_document_clear_declaration,
|
|
392
|
+
[:leptris_document], :leptris_status
|
|
393
|
+
attach_function :leptris_document_remove_doctype,
|
|
394
|
+
[:leptris_document], :leptris_status
|
|
384
395
|
attach_function :leptris_document_append_pi,
|
|
385
396
|
[:leptris_document, :string, :string], :leptris_node_ref
|
|
386
397
|
attach_function :leptris_document_remove_child,
|
|
@@ -849,6 +860,14 @@ attach_function :leptris_parse_string,
|
|
|
849
860
|
[:leptris_sax_parser], :void
|
|
850
861
|
attach_function :leptris_sax_parser_set_streaming,
|
|
851
862
|
[:leptris_sax_parser, :int], :int
|
|
863
|
+
# One-shot input hint (libleptris 1.9.204): whole document in
|
|
864
|
+
# a single feed(), alive for the parser's lifetime — enables
|
|
865
|
+
# in-place scans. The binding's one-shot entry
|
|
866
|
+
# (leptris_sax_parse) is one-shot by construction; the engine
|
|
867
|
+
# sets this itself for pull's memory source. Attach-only
|
|
868
|
+
# until a chunked-feed face wants to opt in.
|
|
869
|
+
attach_function :leptris_sax_parser_set_one_shot,
|
|
870
|
+
[:leptris_sax_parser, :int], :void
|
|
852
871
|
|
|
853
872
|
# Pull parsing (v1.6.0): StAX-style cursor over a document.
|
|
854
873
|
attach_function :leptris_pull_new,
|
|
@@ -1024,6 +1043,30 @@ attach_function :leptris_parse_string,
|
|
|
1024
1043
|
attach_function :leptris_str_has_nonstandard_entity,
|
|
1025
1044
|
[:string, :size_t], :int
|
|
1026
1045
|
|
|
1046
|
+
# DTD validation (exported 1.9.202; the surface existed
|
|
1047
|
+
# since the CLI work): parse an internal subset, merge an
|
|
1048
|
+
# external subset (application-owned I/O), validate a
|
|
1049
|
+
# document. Errors carry message/element/line/column.
|
|
1050
|
+
class DTDErrorStruct < ::FFI::Struct
|
|
1051
|
+
layout :message, :string,
|
|
1052
|
+
:element_name, :string,
|
|
1053
|
+
:line, :int,
|
|
1054
|
+
:column, :int
|
|
1055
|
+
end
|
|
1056
|
+
attach_function :leptris_dtd_parse,
|
|
1057
|
+
[:string, :size_t], :pointer
|
|
1058
|
+
# leptris_document_get_dtd attached above with the readers
|
|
1059
|
+
attach_function :leptris_dtd_parse_external_subset,
|
|
1060
|
+
[:pointer, :string, :size_t], :int
|
|
1061
|
+
attach_function :leptris_dtd_set_pe_loader,
|
|
1062
|
+
[:pointer, :pointer, :pointer], :void
|
|
1063
|
+
attach_function :leptris_dtd_validate,
|
|
1064
|
+
[:leptris_document, :pointer, :pointer], :int
|
|
1065
|
+
attach_function :leptris_dtd_free,
|
|
1066
|
+
[:pointer], :void
|
|
1067
|
+
attach_function :leptris_dtd_error_free,
|
|
1068
|
+
[:pointer], :void
|
|
1069
|
+
|
|
1027
1070
|
attach_function :leptris_rng_parse,
|
|
1028
1071
|
[:string, :size_t, :pointer], :leptris_relaxng
|
|
1029
1072
|
attach_function :leptris_rng_parse_file,
|
|
@@ -1100,6 +1143,11 @@ attach_function :leptris_parse_string,
|
|
|
1100
1143
|
[:leptris_doctype], :string
|
|
1101
1144
|
|
|
1102
1145
|
attach_function :leptris_free_string, [:pointer], :void
|
|
1146
|
+
# Engine-heap allocation (libleptris 1.9.204): buffers handed
|
|
1147
|
+
# to engine callbacks (the DTD PE loader) so the engine frees
|
|
1148
|
+
# through its matching deallocation path. Release with
|
|
1149
|
+
# leptris_free_string when NOT consumed by the engine.
|
|
1150
|
+
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1103
1151
|
attach_function :leptris_explicit_cleanup, [], :void
|
|
1104
1152
|
attach_function :leptris_set_memory_management_functions,
|
|
1105
1153
|
[:pointer, :pointer], :void
|
|
@@ -1109,6 +1157,33 @@ attach_function :leptris_parse_string,
|
|
|
1109
1157
|
[:leptris_document, :pointer, :pointer], :leptris_status
|
|
1110
1158
|
attach_function :leptris_status_string, [:leptris_status], :string
|
|
1111
1159
|
attach_function :leptris_error_message, [:leptris_status], :string
|
|
1160
|
+
# Engine surface adopted with libleptris 1.9.206 (the FFI
|
|
1161
|
+
# mirror audit keeps attachments and exports in lockstep):
|
|
1162
|
+
# recover diagnostics (#1200), the standalone DTD family
|
|
1163
|
+
# (leptris_dtd_*), declaration/doctype removal (#1094), the
|
|
1164
|
+
# engine-heap buffer (#1219), one-shot SAX (#1178), and the
|
|
1165
|
+
# exported error-recorder entry point.
|
|
1166
|
+
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1167
|
+
attach_function :leptris_document_clear_declaration,
|
|
1168
|
+
[:leptris_document], :leptris_status
|
|
1169
|
+
attach_function :leptris_document_remove_doctype,
|
|
1170
|
+
[:leptris_document], :leptris_status
|
|
1171
|
+
attach_function :leptris_document_parse_diag_count,
|
|
1172
|
+
[:leptris_document], :size_t
|
|
1173
|
+
attach_function :leptris_document_parse_diag,
|
|
1174
|
+
[:leptris_document, :size_t, :pointer, :pointer, :size_t], :int
|
|
1175
|
+
attach_function :leptris_dtd_parse,
|
|
1176
|
+
[:string, :size_t], :leptris_dtd
|
|
1177
|
+
attach_function :leptris_dtd_free, [:leptris_dtd], :void
|
|
1178
|
+
attach_function :leptris_dtd_parse_external_subset,
|
|
1179
|
+
[:leptris_dtd, :string, :size_t], :int
|
|
1180
|
+
attach_function :leptris_dtd_set_pe_loader,
|
|
1181
|
+
[:leptris_dtd, :pointer, :pointer], :void
|
|
1182
|
+
attach_function :leptris_dtd_validate,
|
|
1183
|
+
[:leptris_document, :leptris_dtd, :pointer], :int
|
|
1184
|
+
attach_function :leptris_dtd_error_free, [:pointer], :void
|
|
1185
|
+
attach_function :leptris_sax_parser_set_one_shot,
|
|
1186
|
+
[:leptris_sax_parser, :int], :void
|
|
1112
1187
|
# Thread-global last-failure position (error.h): companion to
|
|
1113
1188
|
# leptris_last_error; populated by recover parses (#547).
|
|
1114
1189
|
attach_function :leptris_last_error_position,
|
data/lib/leptris/xml.rb
CHANGED
|
@@ -34,6 +34,7 @@ module Leptris
|
|
|
34
34
|
autoload :XSLT, "leptris/xml/xslt"
|
|
35
35
|
autoload :XQuery, "leptris/xml/xquery"
|
|
36
36
|
autoload :RelaxNG, "leptris/xml/relaxng"
|
|
37
|
+
autoload :DTD, "leptris/xml/dtd"
|
|
37
38
|
autoload :Pull, "leptris/xml/pull"
|
|
38
39
|
autoload :Iterparse, "leptris/xml/iterparse"
|
|
39
40
|
|
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.207] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- stop leptris_set_error leaking into the ELF dynamic table (build)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [1.9.206] - 2026-09-19
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- endian-neutral close-tag name compare (#1197) (parse)
|
|
16
|
+
|
|
17
|
+
### Performance
|
|
18
|
+
|
|
19
|
+
- memchr finds the text span end (TODO.max-perf/2-3 slice 3) (sax)
|
|
20
|
+
- zero-copy slice emission for one-shot pulls (#1178 tail) (sax)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.9.205] - 2026-09-19
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Parse recover diagnostics (#1200): the DOM parse now records
|
|
29
|
+
duplicate-attribute events on the document — `LeptrisDiagKind`
|
|
30
|
+
(with the new `LEPTRIS_DIAG_RECOVER` kind) is public in
|
|
31
|
+
types.h, and the diag list reads through
|
|
32
|
+
`leptris_document_parse_diag_count` /
|
|
33
|
+
`leptris_document_parse_diag` (error.h). First definition wins,
|
|
34
|
+
matching the SAX lane's libxml2-recover surface; the digest
|
|
35
|
+
gate no longer needs a SAX probe pass.
|
|
36
|
+
- `LEPTRIS_DIGEST_ATTR_ORDER` (#1200 part 1): opt-in digest flag
|
|
37
|
+
that makes attribute order identity-bearing instead of sorted
|
|
38
|
+
away.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- MSVC C2027 in `leptris_internal.h`: the `LeptrisDiag` forward
|
|
43
|
+
declaration moved to file scope (broke the windows-dll-exports
|
|
44
|
+
leg).
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
- First-char-bucketed tag classifier for the WHATWG HTML path:
|
|
49
|
+
the tokenizer's name-list strcmp scans become one-load bucket
|
|
50
|
+
lookups — parse +31-37% on the #1218 bench shapes.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## [1.9.204] - 2026-09-19
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- leptris/html.h subsystem header; one-shot name fast paths (slice 1); document HTML parsing (api+perf)
|
|
59
|
+
|
|
60
|
+
### Performance
|
|
61
|
+
|
|
62
|
+
- buffer-mode pull feeds in ONE call (slice-1 prerequisite) (sax)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## [1.9.203] - 2026-09-19
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- leptris_dtd_parse honors the NULL-on-error contract (#1211) (dtd)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [1.9.202] - 2026-09-18
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- export the DTD validation surface; gate unmarked public declarations (api)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
3
82
|
## [1.9.201] - 2026-09-18
|
|
4
83
|
|
|
5
84
|
<!-- Edit this section with the actual release notes. -->
|
|
@@ -69,7 +69,7 @@ Core (src/leptris/) → DOM, parser, XPath engine, SAX, DTD, encoding, memor
|
|
|
69
69
|
- `xinclude/` — built and shipping. `parse="text"` with `xi:fallback` works end-to-end. `parse="xml"` and xpointer are TODO 92.
|
|
70
70
|
|
|
71
71
|
### Public API surface (`src/include/leptris/`)
|
|
72
|
-
The public contract. Don't break ABI without a major bump. Headers split by subsystem: `dom/{document,element,serialize}.h`, `xpath/xpath.h`, `sax/sax.h`, plus `types.h`, `error.h`, `dtd.h` at the top level. All handles are opaque typedefs (`LeptrisDocument`, `LeptrisElement`, `LeptrisNodeRef`, …); callers never see struct definitions.
|
|
72
|
+
The public contract. Don't break ABI without a major bump. Headers split by subsystem: `dom/{document,element,serialize}.h`, `xpath/xpath.h`, `sax/sax.h`, `html.h`, plus `types.h`, `error.h`, `dtd.h` at the top level. All handles are opaque typedefs (`LeptrisDocument`, `LeptrisElement`, `LeptrisNodeRef`, …); callers never see struct definitions.
|
|
73
73
|
|
|
74
74
|
### Memory model
|
|
75
75
|
- Pool allocator is the dominant allocation path — node creation should go through `leptris_node_create_pooled`.
|
|
@@ -320,6 +320,32 @@ if (r) {
|
|
|
320
320
|
Supported: all 13 axes, all 27 functions, all 15 operators, full
|
|
321
321
|
predicate syntax. See <<api,xpath-coverage>> for details.
|
|
322
322
|
|
|
323
|
+
==== HTML parsing (WHATWG + libxml2-compat)
|
|
324
|
+
|
|
325
|
+
HTML is a first-class input — two tolerant parsers, both declared in
|
|
326
|
+
`leptris/html.h`:
|
|
327
|
+
|
|
328
|
+
[source,c]
|
|
329
|
+
----
|
|
330
|
+
LeptrisStatus st;
|
|
331
|
+
/* WHATWG-conformant tree construction (html5lib corpus green). */
|
|
332
|
+
LeptrisDocument doc =
|
|
333
|
+
leptris_parse_html_string("<p>Hello <b>world", 18, &st);
|
|
334
|
+
|
|
335
|
+
/* Nokogiri byte-for-byte shape: no synthesized <html>/<head>/<body>,
|
|
336
|
+
* no implied <tbody>, leading script/style content stays in <body>. */
|
|
337
|
+
LeptrisDocument frag =
|
|
338
|
+
leptris_parse_html4_string("<p>Hello <b>world", 18, &st);
|
|
339
|
+
----
|
|
340
|
+
|
|
341
|
+
Both handle implied end tags (`p`/`li`/`td`/`tr`/`th`/`dt`/`dd`/
|
|
342
|
+
`option`/...), void elements, raw-text `script`/`style`, minimized
|
|
343
|
+
and unquoted attribute values, case-insensitive tag/attribute names,
|
|
344
|
+
and the HTML named-entity table. Malformed input never fails the
|
|
345
|
+
parse — it degrades to text; the result serializes and queries
|
|
346
|
+
(XPath/XSLT) like any XML document. Memory contract identical to
|
|
347
|
+
`leptris_parse_string`.
|
|
348
|
+
|
|
323
349
|
==== Parsing models: DOM, SAX, StAX, iterparse
|
|
324
350
|
|
|
325
351
|
One engine, four consumption models. Pick by how you want to pay:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Levers 2+3 — compiled-dispatch + zero-copy one-shot emission (design)
|
|
2
|
+
|
|
3
|
+
Status: DESIGNED (2026-09-19). Implementation slices below.
|
|
4
|
+
Owners: engine repo. Bench: `benchmarks/sax/bench_iterparse.c` (#1195).
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
The pull/iterparse path costs 4.4x the compact DOM parse over the
|
|
9
|
+
same bytes (10KB medium: pull 54.7us vs DOM one-shot 12.3us).
|
|
10
|
+
Root cause (measured, #1178): every event string is copied twice —
|
|
11
|
+
|
|
12
|
+
1. streaming.c copies names/text/attrs into per-frame storage
|
|
13
|
+
(scratch arena + inline name bufs), because a streaming feed()
|
|
14
|
+
chunk may be freed once the call returns;
|
|
15
|
+
2. pull.c's cb_* strdups them again into queue events, freed at the
|
|
16
|
+
next _next.
|
|
17
|
+
|
|
18
|
+
A falsified attempt (per-call arena staging in _next) ADDED a third
|
|
19
|
+
copy layer and measured slower (54.7 -> 67.6us) — churn-shaving the
|
|
20
|
+
wrong layer cannot win; the copies must not happen at all.
|
|
21
|
+
|
|
22
|
+
## Insight
|
|
23
|
+
|
|
24
|
+
One-shot parses (leptris_sax_parse / leptris_pull_new over an
|
|
25
|
+
in-memory buffer) have no interruption: the input buffer is
|
|
26
|
+
immutable (#1125), caller-owned, and alive for the whole iteration
|
|
27
|
+
(the pull API already documents input-lifetime for buffer mode).
|
|
28
|
+
Names and attribute names cannot contain entity references (XML
|
|
29
|
+
names are name-chars only), so in one-shot mode they are always
|
|
30
|
+
contiguous slices of the input — borrowable with zero copies.
|
|
31
|
+
|
|
32
|
+
Only entity-expanded payloads (attribute values, text with &...;)
|
|
33
|
+
must materialize; the common case has none.
|
|
34
|
+
|
|
35
|
+
## Design
|
|
36
|
+
|
|
37
|
+
### Slice events (one-shot only)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
typedef struct {
|
|
41
|
+
const char* ptr; /* points INTO the input buffer */
|
|
42
|
+
size_t len;
|
|
43
|
+
} LeptrisSlice;
|
|
44
|
+
|
|
45
|
+
typedef struct {
|
|
46
|
+
LeptrisPullEventType type;
|
|
47
|
+
LeptrisSlice name; /* element / PI target */
|
|
48
|
+
LeptrisSlice text; /* ONLY if no entities: borrowed */
|
|
49
|
+
char* text_owned; /* materialized when entities */
|
|
50
|
+
LeptrisSlice attr_flat[]; /* name,value pairs, names always
|
|
51
|
+
borrowed; values borrowed when
|
|
52
|
+
entity-free, else owned */
|
|
53
|
+
} OneShotEvent;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Rule: a payload is borrowed iff the engine can prove (single scan,
|
|
57
|
+
memchr '&') that its span contains no '&'. One memchr per payload
|
|
58
|
+
replaces one malloc+memcpy+free — and '&' is absent from the vast
|
|
59
|
+
majority of real payloads.
|
|
60
|
+
|
|
61
|
+
### Compiled dispatch (lever 2)
|
|
62
|
+
|
|
63
|
+
The streaming machine's per-state if-chains (peek '<', '/', '!',
|
|
64
|
+
name-start, ws...) become a 256-entry class table + a state-indexed
|
|
65
|
+
goto/switch jump table:
|
|
66
|
+
|
|
67
|
+
- class LUT: 4 bits (WS, NAME_START, NAME_CHAR, SPECIAL masks) —
|
|
68
|
+
one load classifies each byte (the #1177 LUT result generalized);
|
|
69
|
+
- state dispatch: `switch (state)` compiled to a jump table by the
|
|
70
|
+
compiler once states are a dense enum — keep cases small and
|
|
71
|
+
branchless-friendly (the hot tokenizer states are ~20).
|
|
72
|
+
|
|
73
|
+
The SAME token loop serves both modes: streaming keeps frames/carry
|
|
74
|
+
(one_shot=0); one-shot mode (one_shot=1) skips carry and frame
|
|
75
|
+
copies, emits slices, and materializes only entity spans.
|
|
76
|
+
|
|
77
|
+
### Consumer adaptation
|
|
78
|
+
|
|
79
|
+
pull.c gains a one-shot queue variant: cb_* check parser->one_shot
|
|
80
|
+
and enqueue slice events (no strdup); queue_reset_event frees only
|
|
81
|
+
owned pointers. Batch staging (#589) copies out of slices on
|
|
82
|
+
demand — the existing stage chain unchanged.
|
|
83
|
+
|
|
84
|
+
## Slices
|
|
85
|
+
|
|
86
|
+
1. **Slice events + one-shot flag** in streaming.c; pull one-shot
|
|
87
|
+
queue. Acceptance: bench_iterparse full-doc 0.47us/event ->
|
|
88
|
+
<=0.15us/event; no test changes (contracts identical).
|
|
89
|
+
2. **Class LUT + dense-state dispatch** in the tokenizer loop.
|
|
90
|
+
Acceptance: bench_sax +5% minimum; no behavioral delta.
|
|
91
|
+
3. **memchr fast paths** for text/attr-value spans feeding the
|
|
92
|
+
borrow check. Acceptance: entity-free docs near-memcpy speed.
|
|
93
|
+
|
|
94
|
+
## Non-goals
|
|
95
|
+
|
|
96
|
+
- No API changes; event lifetime contracts unchanged.
|
|
97
|
+
- Streaming feed() semantics untouched (frames/carry stay).
|
|
98
|
+
- No new allocation strategies in pull.c (the falsified lesson).
|