leptris 1.9.201.4 → 1.9.204.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 +47 -0
- data/lib/leptris/xml.rb +1 -0
- data/vendor-src/libleptris/CHANGELOG.md +28 -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 +7 -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/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/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/html/html_parse.c +31 -3
- data/vendor-src/libleptris/src/leptris/leptris.c +4 -2
- data/vendor-src/libleptris/src/leptris/sax/pull.c +10 -1
- data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +7 -0
- data/vendor-src/libleptris/src/leptris/sax/streaming.c +66 -0
- 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_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/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: 0facebf2d44046c5a2c7ddaca5fdc8d2b5aea2b92017f6f1de8844122ec0eda4
|
|
4
|
+
data.tar.gz: f0f09d7786bd551d6f7077e1112e499ad25d6236a379d10607f59aeb2ddef02e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbb0f3100ba9c705da214daafe066dcdbd973e41abb08d04e4b6db69a18091976b0083e223c9e888829fc46e5b6c4fe49ddf7d3defa380e62631fdc2d3897f7b
|
|
7
|
+
data.tar.gz: e11a0141773522b79b5ddc5217aa706b1ef01517e8981924842e0f1b7d272499601b3723d6df23c2741dea1e5e5733818a8b743a67af19b63be0f4ce9a554c10
|
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
|
+
3.4 CI legs** — filed upstream as leptris/leptris#1242
|
|
58
|
+
(leptris-ruby#279 tracks the investigation). Six strikes; the
|
|
59
|
+
last three were consecutive reruns of one job, suggesting
|
|
60
|
+
per-machine determinism. Unreproducible across ~75k local reads
|
|
61
|
+
on darwin/arm64 (native + FFI, GC.stress) and x86_64 linux
|
|
62
|
+
(qemu; native + FFI; threaded and not). The leg sets
|
|
63
|
+
`LEPTRIS_SKIP_1242=1` to quarantine exactly that one spec on
|
|
64
|
+
that one leg until the engine ships the fix; every other leg
|
|
65
|
+
keeps the full guard.
|
|
66
|
+
|
|
67
|
+
- **#1220 reopened with the 1.9.203 repro**: HTML-document
|
|
68
|
+
mutations are still lost on serialization (the exact original
|
|
69
|
+
repro stands; node reads back mutated, serializer sees the old
|
|
70
|
+
tree). Tracking continues upstream.
|
|
71
|
+
- **#1197 reopened with fresh 1.9.203 evidence**: the s390x qemu
|
|
72
|
+
leg failed 442/742 with the identical `malformed input`
|
|
73
|
+
signature at ASCII offsets; the reopen quotes the close
|
|
74
|
+
comment's own terms. (1.9.204's notes carry no BE fix; the leg
|
|
75
|
+
stays experimental until the engine ships one.)
|
|
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.204"
|
|
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
|
|
@@ -381,6 +385,12 @@ attach_function :leptris_parse_string,
|
|
|
381
385
|
[:leptris_document, :int], :leptris_status
|
|
382
386
|
attach_function :leptris_document_set_doctype,
|
|
383
387
|
[:leptris_document, :string, :string, :string], :leptris_doctype
|
|
388
|
+
# The #1229 remove-half (libleptris 1.9.204): un-set the
|
|
389
|
+
# declaration; un-set the DOCTYPE (NOT_FOUND when none set).
|
|
390
|
+
attach_function :leptris_document_clear_declaration,
|
|
391
|
+
[:leptris_document], :leptris_status
|
|
392
|
+
attach_function :leptris_document_remove_doctype,
|
|
393
|
+
[:leptris_document], :leptris_status
|
|
384
394
|
attach_function :leptris_document_append_pi,
|
|
385
395
|
[:leptris_document, :string, :string], :leptris_node_ref
|
|
386
396
|
attach_function :leptris_document_remove_child,
|
|
@@ -849,6 +859,14 @@ attach_function :leptris_parse_string,
|
|
|
849
859
|
[:leptris_sax_parser], :void
|
|
850
860
|
attach_function :leptris_sax_parser_set_streaming,
|
|
851
861
|
[:leptris_sax_parser, :int], :int
|
|
862
|
+
# One-shot input hint (libleptris 1.9.204): whole document in
|
|
863
|
+
# a single feed(), alive for the parser's lifetime — enables
|
|
864
|
+
# in-place scans. The binding's one-shot entry
|
|
865
|
+
# (leptris_sax_parse) is one-shot by construction; the engine
|
|
866
|
+
# sets this itself for pull's memory source. Attach-only
|
|
867
|
+
# until a chunked-feed face wants to opt in.
|
|
868
|
+
attach_function :leptris_sax_parser_set_one_shot,
|
|
869
|
+
[:leptris_sax_parser, :int], :void
|
|
852
870
|
|
|
853
871
|
# Pull parsing (v1.6.0): StAX-style cursor over a document.
|
|
854
872
|
attach_function :leptris_pull_new,
|
|
@@ -1024,6 +1042,30 @@ attach_function :leptris_parse_string,
|
|
|
1024
1042
|
attach_function :leptris_str_has_nonstandard_entity,
|
|
1025
1043
|
[:string, :size_t], :int
|
|
1026
1044
|
|
|
1045
|
+
# DTD validation (exported 1.9.202; the surface existed
|
|
1046
|
+
# since the CLI work): parse an internal subset, merge an
|
|
1047
|
+
# external subset (application-owned I/O), validate a
|
|
1048
|
+
# document. Errors carry message/element/line/column.
|
|
1049
|
+
class DTDErrorStruct < ::FFI::Struct
|
|
1050
|
+
layout :message, :string,
|
|
1051
|
+
:element_name, :string,
|
|
1052
|
+
:line, :int,
|
|
1053
|
+
:column, :int
|
|
1054
|
+
end
|
|
1055
|
+
attach_function :leptris_dtd_parse,
|
|
1056
|
+
[:string, :size_t], :pointer
|
|
1057
|
+
# leptris_document_get_dtd attached above with the readers
|
|
1058
|
+
attach_function :leptris_dtd_parse_external_subset,
|
|
1059
|
+
[:pointer, :string, :size_t], :int
|
|
1060
|
+
attach_function :leptris_dtd_set_pe_loader,
|
|
1061
|
+
[:pointer, :pointer, :pointer], :void
|
|
1062
|
+
attach_function :leptris_dtd_validate,
|
|
1063
|
+
[:leptris_document, :pointer, :pointer], :int
|
|
1064
|
+
attach_function :leptris_dtd_free,
|
|
1065
|
+
[:pointer], :void
|
|
1066
|
+
attach_function :leptris_dtd_error_free,
|
|
1067
|
+
[:pointer], :void
|
|
1068
|
+
|
|
1027
1069
|
attach_function :leptris_rng_parse,
|
|
1028
1070
|
[:string, :size_t, :pointer], :leptris_relaxng
|
|
1029
1071
|
attach_function :leptris_rng_parse_file,
|
|
@@ -1100,6 +1142,11 @@ attach_function :leptris_parse_string,
|
|
|
1100
1142
|
[:leptris_doctype], :string
|
|
1101
1143
|
|
|
1102
1144
|
attach_function :leptris_free_string, [:pointer], :void
|
|
1145
|
+
# Engine-heap allocation (libleptris 1.9.204): buffers handed
|
|
1146
|
+
# to engine callbacks (the DTD PE loader) so the engine frees
|
|
1147
|
+
# through its matching deallocation path. Release with
|
|
1148
|
+
# leptris_free_string when NOT consumed by the engine.
|
|
1149
|
+
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1103
1150
|
attach_function :leptris_explicit_cleanup, [], :void
|
|
1104
1151
|
attach_function :leptris_set_memory_management_functions,
|
|
1105
1152
|
[:pointer, :pointer], :void
|
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,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.204] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- leptris/html.h subsystem header; one-shot name fast paths (slice 1); document HTML parsing (api+perf)
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- buffer-mode pull feeds in ONE call (slice-1 prerequisite) (sax)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [1.9.203] - 2026-09-19
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- leptris_dtd_parse honors the NULL-on-error contract (#1211) (dtd)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [1.9.202] - 2026-09-18
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- export the DTD validation surface; gate unmarked public declarations (api)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## [1.9.201] - 2026-09-18
|
|
4
32
|
|
|
5
33
|
<!-- 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).
|
|
@@ -77,8 +77,10 @@ static void print_element_text(LeptrisElement e) {
|
|
|
77
77
|
fputs(t, stdout);
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
for (LeptrisNodeRef c = leptris_element_first_child_any(e);
|
|
81
|
-
c
|
|
80
|
+
for (LeptrisNodeRef c = (LeptrisNodeRef)leptris_element_first_child_any(e);
|
|
81
|
+
c;
|
|
82
|
+
c = (LeptrisNodeRef)leptris_element_next_sibling_any(
|
|
83
|
+
(LeptrisElement)c)) {
|
|
82
84
|
const char* txt = leptris_text_node_get_content(c);
|
|
83
85
|
if (txt) fputs(txt, stdout);
|
|
84
86
|
}
|
|
@@ -303,6 +303,36 @@ void find_by_attribute(LeptrisElement parent,
|
|
|
303
303
|
}
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
+
## Parsing HTML
|
|
307
|
+
|
|
308
|
+
HTML is a first-class input: the WHATWG-conformant tolerant parser
|
|
309
|
+
(`leptris_parse_html_string`) plus a libxml2/Nokogiri-compatibility
|
|
310
|
+
mode (`leptris_parse_html4_string`), both declared in
|
|
311
|
+
`leptris/html.h`:
|
|
312
|
+
|
|
313
|
+
```c
|
|
314
|
+
#include "leptris/html.h"
|
|
315
|
+
|
|
316
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
317
|
+
LeptrisDocument doc =
|
|
318
|
+
leptris_parse_html_string("<p>Hello <b>world", 18, &st);
|
|
319
|
+
/* Implied end tags, void elements, raw-text <script>/<style>,
|
|
320
|
+
* minimized + unquoted attribute values, case-insensitive names and
|
|
321
|
+
* the HTML named-entity table are all handled. Malformed input
|
|
322
|
+
* never fails the parse — it degrades to text. The result
|
|
323
|
+
* serializes and queries (XPath/XSLT) like any XML document. */
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Choose the mode by contract:
|
|
327
|
+
|
|
328
|
+
| Function | Mode | Shape |
|
|
329
|
+
|----------|------|-------|
|
|
330
|
+
| `leptris_parse_html_string` | WHATWG | full html5lib-conformant tree construction |
|
|
331
|
+
| `leptris_parse_html4_string` | libxml2/Nokogiri | fragment shape: no synthesized `<html>/<head>/<body>`, no implied `<tbody>`; leading `<script>/<style>` content stays in `<body>` |
|
|
332
|
+
|
|
333
|
+
Memory: identical to XML — `leptris_document_free` releases
|
|
334
|
+
everything; input must be valid UTF-8.
|
|
335
|
+
|
|
306
336
|
## Performance Tips
|
|
307
337
|
|
|
308
338
|
1. **Use in-place parsing** for string data when possible
|
|
@@ -31,6 +31,22 @@ def declared_public_symbols(root: Path):
|
|
|
31
31
|
return names
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def unmarked_public_declarations(root: Path):
|
|
35
|
+
"""Public-header leptris_* function declarations WITHOUT
|
|
36
|
+
LEPTRIS_API: they link fine into the static lib (the CLI works)
|
|
37
|
+
but never export for FFI consumers — the dtd.h class caught by
|
|
38
|
+
the ruby binding (#1183)."""
|
|
39
|
+
pat = re.compile(
|
|
40
|
+
r"^(?!LEPTRIS_API|static|typedef|struct|#|//|\*|/\*)(?:const\s+|unsigned\s+|struct\s+)*"
|
|
41
|
+
r"[A-Za-z_][\w\s\*]*?\b(leptris_[a-z_0-9]+)\s*\(",
|
|
42
|
+
flags=re.M,
|
|
43
|
+
)
|
|
44
|
+
found = set()
|
|
45
|
+
for h in (root / "src" / "include").rglob("*.h"):
|
|
46
|
+
for m in pat.finditer(h.read_text()):
|
|
47
|
+
found.add(m.group(1))
|
|
48
|
+
return found
|
|
49
|
+
|
|
34
50
|
def exported_symbols(lib: Path):
|
|
35
51
|
out = subprocess.run(
|
|
36
52
|
["nm", "-gU", str(lib)], capture_output=True, text=True, check=True
|
|
@@ -65,11 +81,15 @@ def main():
|
|
|
65
81
|
undeclared = sorted(exported - declared)
|
|
66
82
|
missing = sorted(declared - exported)
|
|
67
83
|
|
|
84
|
+
unmarked = sorted(unmarked_public_declarations(root) - declared)
|
|
85
|
+
|
|
68
86
|
failures = []
|
|
69
87
|
for n in undeclared:
|
|
70
88
|
failures.append(f"UNDECLARED EXPORT {n}: exported but absent from src/include/")
|
|
71
89
|
for n in missing:
|
|
72
90
|
failures.append(f"MISSING EXPORT {n}: declared public but not exported")
|
|
91
|
+
for n in unmarked:
|
|
92
|
+
failures.append(f"UNMARKED PUBLIC {n}: declared in a public header without LEPTRIS_API")
|
|
73
93
|
|
|
74
94
|
if failures:
|
|
75
95
|
print("EXPORT SURFACE DRIFT:")
|