A streaming HTML rewriter for Zig — a pure-Zig port of Cloudflare's
lol-html, the engine behind the
HTMLRewriter API in Cloudflare Workers and Bun. Built for cottontail,
where it backs the HTMLRewriter API without a Rust toolchain or C
bindings in the build.
Parse and transform HTML as it streams through, with CSS-selector-driven handlers and byte-preserving output: content you don't touch passes through byte-identical, at any chunk size, without buffering the document or building a DOM.
const html_rewriter = @import("html_rewriter");
fn onElement(_: ?*anyopaque, el: *html_rewriter.units.element.Element) anyerror!void {
try el.setAttribute("data-seen", "1");
try el.prepend("<!-- hi -->", .html);
}
const out = try html_rewriter.rewriteStr(allocator, "<div class=x>hello</div>", .{
.element_content_handlers = &.{.{
.selector = "div.x",
.handlers = .{ .element = .{ .ctx = null, .func = &onElement } },
}},
});
// streaming: html_rewriter.HtmlRewriter.init(...) + write(chunk) / end()Behavior is ported and verified; internals are idiomatic Zig rather than transliterated Rust. Output parity with upstream is enforced by CI-run suites (all green at every chunk size, including 1-byte chunks):
- html5lib tokenizer conformance — 7,028 cases
- Parser feedback (foreign content / text-mode switching, derived from html5lib tree-construction, expected tokens computed by html5ever) — 1,811 cases, including strict-mode ambiguity bailouts
- Selector parse accept/reject parity — exact match with upstream's supported selector grammar on all 346 fixture selectors
- Upstream functional fixtures — selector_matching (784) and element_content_replacement (784)
- Differential testing — 2,544 rewrite runs compared byte-for-byte against the original Rust implementation across an operation × selector × document matrix
Scope: UTF-8 only (upstream only supports ASCII-compatible encodings; this port drops the encoding layer entirely) and no C API — it's a Zig module. See COMPAT.md for the API surface and known gaps, and DESIGN.md for architecture and test methodology.
Zig 0.16. In-tree development uses the sibling cottontail repo's vendored toolchain:
../cottontail/vendors/zig/zig build test # everything
../cottontail/vendors/zig/zig build test -Dchunk-size=1 # boundary stresstests/data/ vendors the upstream test corpus (html5lib-tests,
selector fixtures, regression inputs). tests/data/generated/ is produced
by tools/oracle (Rust; build-time only) and committed, which keeps the
Zig test suite free of any Rust dependency:
cd tools/oracle
cargo run --release -- ../../tests/data ../../tests/data/generated
cargo run --release --bin differential -- ../../tests/data ../../tests/data/generatedtests/upstream-reference/ keeps upstream's Rust harness sources for
reference; they are not built.
BSD 3-Clause, same license as lol-html — see LICENSE.
This project is a port of lol-html (© 2019 Cloudflare, Inc.) and derives its design and observable behavior from it; it also redistributes test material from lol-html, html5lib-tests, and the W3C CSS3 Selectors test suite. See THIRD_PARTY_NOTICES.