Repository navigation
Comparison with Lexbor
Parsec's aim is to be the parser Lexbor's users would pick instead. That only works if we're straight about where things stand, so this page goes through Lexbor module by module: what Lexbor has, what Parsec has today, and what's planned, with the issue for each.
On this page:
- The short version
- Module by module: html, dom, selectors, css and style, encoding, url, punycode, unicode, ns and tag, core, ports, engine, utils, Builds, bindings and tooling, What neither has: XPath
- Safety
- Speed
- Sources
Note
Everything said about Lexbor here links to its source at commit 145872a (2026-10-26), its issue tracker, or its documentation. In the module sections, small numbers do the linking so the sentences stay readable; Sources lists them all. If something has changed since, please open an issue and we'll fix the page.
Lexbor is a mature C library: released since 2018, with HTML, DOM, CSS, selectors, encodings, URL and Unicode modules, used inside PHP 8.4's Dom\HTMLDocument, selectolax, nokolexbor and more, and tested against 200 million pages under AddressSanitizer by its own account (README). Parsec is unreleased, and today it covers one of those areas: HTML parsing, the tree, selectors and serialization. Within that area it already does some things Lexbor doesn't. Everything else is planned.
| Lexbor | Parsec today | Parsec planned | Parsec | |
|---|---|---|---|---|
| Released, packaged | Yes: 3.0.0, in vcpkg, Homebrew, Debian and others | No; from git | 0.1.0 on crates.io, PyPI and npm (#32) | 📋 |
| HTML parsing, fragments | Yes | Yes | ✅ | |
| Bytes in, encoding sniffed | Yes | No, &str only |
#5 | 📋 |
| Chunked input | Yes | No | #6 | 📋 |
| Parse errors | With spec ids, as pointers | No | With names, line and column (#7) | 📋 |
| Source positions on nodes | No | No | #8 | 📋 |
| Limits on hostile input | None (lexbor#340) | Time budget, size cap | Depth limit (#10) | ✅ 📋 |
| Thread-safe documents | No (lexbor#105) |
Send and Sync today, not yet promised |
Promised and checked at compile time (#11) | 📋 |
| Node lifetime | Raw pointers (lexbor#132) | Indexes that can't dangle | Indexes that know when their node was removed (#12) | 📋 |
| CSS selectors | Level 4; many pseudo-classes parse and never match | Level 4, including :scope
|
The rest a static page can decide (M3) | ✅ 📋 |
| XPath | No | No | #31 | 📋 |
| Encodings | 40 | None | #25 | 📋 |
| URL | Yes | No | #26 | 📋 |
| CSS parser, CSSOM | Yes | No | #28, #29, #30 | 📋 |
| C API | Yes, it's C | No | #20, #21 | 📋 |
| Python, WebAssembly | Through others' bindings; WASM build | No | Built here (#23, #24) | 📋 |
The last column uses the marks from Home: ✅ shipped and checked by cargo test, 🚧 in progress, 📋 planned. A row with both has a part of each. Thread-safe documents and node lifetime get no ✅ yet: what Parsec does today there is true of the code, but no test checks it.
Lexbor's html module has the WHATWG tokenizer and tree builder; whole-document, fragment and chunked parsing1,2; a scripting flag; the tokenizer on its own with a per-token callback; encoding sniffing with the <meta> prescan3; two serializers, one with pretty-printing and the html5lib test format4,5; parse errors with spec ids, located by pointers into the input rather than line and column6; and per-element interfaces, with <select> selectedness and <selectedcontent> added in 3.0.07.
Parsec today:
- The tokenizer and tree builder, passing the html5lib tokenizer suite and every WPT tree-construction case that doesn't need JavaScript (Conformance).
- Whole-document and fragment parsing, with a scripting flag (Parsing).
- The tokenizer on its own, through a
Sinktrait (Tokenizer). -
innerHTML,outerHTML, a start tag alone, and text (Serialization). -
<select>selectedness and<selectedcontent>. - A time budget and a size cap (Limits and hostile input).
Planned: bytes in with encoding sniffing (#5); chunked input that builds exactly the tree whole input does (#6); parse errors with spec names, line and column (#7); source positions on every node and attribute (#8), which Lexbor users have asked for (lexbor#374, lexbor#388); pretty-printing and the html5lib serializer format (#9); a depth limit (#10).
On hostile input: Lexbor's own issue #340 shows inputs of a few hundred kilobytes taking seconds, minutes, or all of a machine's memory, and Lexbor offers no limit. Parsec is linear on the eight inputs in its hostile suite, deeply nested <div>s among them, has a time budget for whatever else turns up, and has a depth limit planned.
Lexbor's dom module has the WHATWG DOM node types, including ShadowRoot; insertion and removal both unchecked and spec-checked; cloning and importing; lookups by id, tag, class and attribute; textContent get and set; attribute get, set, remove and iterate; and mutation callbacks8,9,10. Lexbor's roadmap marks DOM interfaces as in progress and DOM events as not started11. Nodes are raw pointers with no reference counting, an open issue since 202112.
Parsec today: an arena of nodes addressed by NodeId; parent, child and sibling links; create, insert, move and remove; set, add, rename and remove attributes; rename elements; template contents as the spec has them, or moved into the tree on request (The document tree, Editing). There's no textContent setter, no cloning or importing between documents, no lookups other than selectors, no shadow roots, no mutation callbacks, and no events. Insertion checks for cycles but not every rule the DOM has.
Planned: documents that are Send and Sync, checked at compile time (#11); ids that can tell when their node was removed (#12).
Lexbor's selectors module matches parsed selectors with lxb_selectors_find, lxb_selectors_find_reverse and lxb_selectors_match_node, with options to include the root and stop at the first match. It has all four combinators plus the column combinator, and :has(), :is(), :where(), :not() and :nth-child(of S). A set of pseudo-classes, :scope among them, parse and never match (selectors.c); :scope support is an open pull request since December 2024 (lexbor#257), and there's an open bug where ~ after :is() only checks the next sibling (lexbor#436).
Parsec today: Level 4 selectors, :scope included, matched as a browser would on a static page, with the full list on Selectors.
Planned, in milestone M3: :blank and :defined (#13); :lang() and :dir() (#14); :default, :indeterminate, :valid, :invalid, :in-range and :out-of-range (#15); :target and :local-link given a URL (#16); namespace prefixes, the column combinator and :nth-col() (#17); lazy queries and specificity (#18); the WPT selector tests that don't need layout (#19); a benchmark against Lexbor's matcher (#3).
Lexbor's css module has a CSS Syntax tokenizer and parser, stylesheets and declarations, and 105 typed properties13, with three typed at-rules14. Its style module attaches stylesheets and style="" to elements. Custom properties and var()15, media queries16 and computed, inherited values17 aren't there.
Parsec today: a selector parser, and nothing else of CSS.
Planned, in M7: a CSS Syntax parser that passes css-parsing-tests, with positions on every rule (#28); every longhand, shorthands, custom properties, var() and calc() (#29); the cascade, media queries against an environment you give, and computed values (#30).
Lexbor's encoding module decodes and encodes the WHATWG Encoding Standard's 40 encodings plus replacement and x-user-defined (const.h), with BOM sniffing. There's an open bug where its UTF-8 fast path accepts overlong sequences and surrogates (lexbor#354).
Parsec today: none. It takes &str, and you decode first.
Planned: sniffing (#5) and the encodings themselves, built on encoding_rs, with the WPT encoding tests (#25), in milestones M2 and M5.
Lexbor has a WHATWG URL parser with URLSearchParams (url.h), Punycode (punycode.h), and Unicode normalization and IDNA (unicode.h, idna.h). PHP 8.5's Uri\WhatWg\Url is built on it (RFC). There are open conformance bugs (lexbor#438 to lexbor#442).
Parsec today: none.
Planned, in M6: URL, URLSearchParams, IDNA, Punycode and normalization, with the WPT URL pass counts published (#26), then a benchmark against Lexbor and Ada (#27).
Lexbor keeps namespace URIs and tag names as integer ids in static tables, registering unknown names as it meets them (ns, tag).
Parsec today: namespaces are the Ns and AttrNs enums, and the 124 tags the tree builder treats specially have numbers (known_tag, tag_number); other names are stored as strings (The document tree). Nothing further is planned here.
Lexbor's core has its own arenas and containers, and lets you replace its allocator, which PHP uses to enforce memory_limit (PHP RFC). ports has POSIX and Windows shims, engine is a convenience layer that decodes, parses and styles in one call, and utils has HTTP header and WARC parsers.
Parsec uses Rust's standard library and allocator, and runs its CI on Linux, macOS and Windows. A document's storage is a few vectors sized from the input (Architecture). There's no allocator hook, no engine layer and no HTTP or WARC parsing, and none is planned.
Lexbor builds with CMake, as shared or static libraries per module, with pkg-config files and a single-header amalgamation; it has a WebAssembly build through Emscripten and fuzzers for each module18,19,20. Its CI runs on Ubuntu only21, and its official binary packages are x86-64 only22. The bindings people use, selectolax, nokolexbor, lazy_html and PHP's, are maintained by others.
Parsec today: a Rust crate, a fuzz target run nightly, and CI on three operating systems.
Planned, in M4: a C API with a generated header, pkg-config and CMake files (#20); prebuilt libraries for Linux, macOS and Windows on x86-64 and ARM (#21); a Python package that runs on free-threaded Python (#23); WebAssembly on npm (#24); and a guide for moving from Lexbor (#22), which Moving from Lexbor starts.
Lexbor has no XPath and no XML parser (lexbor#23); nokolexbor ported libxml2's XPath to get one (nokolexbor). Parsec doesn't have it yet either. Netweir has an XPath 1.0 engine, and moving it onto Parsec's tree is #31, in milestone M8.
Lexbor is C. Its tracker has a long run of use-after-free and null-pointer reports (search), and two CVEs in 2026 (CVE-2026-29078, CVE-2026-29079). Its maintainers fix them, and Lexbor runs fuzzers and static analysis, but the class of bug comes with the language and with pointer-based node handles (lexbor#132).
Parsec is Rust with one unsafe block, and its nodes are indexes, so a stale handle can't touch freed memory. Thread safety and memory safety has the details, including what isn't guaranteed yet.
Not measured parser against parser yet. Benchmarks has the one published number, which includes Python, and the plan for an in-process comparison with Lexbor's C (#2).
Each numbered note in the module sections links straight to its source. Here they are in one list. Files are at Lexbor commit 145872a; the rest are issues, discussions and pages as they stood when this was written.
- html: document.h
- html: parser.h
- html: encoding.h
- html: serialize.h
- html: serialize_ext.h
- html: tokenizer/error.h
- html: CHANGELOG
- dom: node.h
- dom: element.h
- dom: document.h
- dom: Lexbor's roadmap
- dom: lexbor#132
- css and style: property/const.h
- css and style: at_rule/const.h
- css and style: lexbor#232
- css and style: Lexbor's roadmap
- css and style: discussion #265
- Builds, bindings and tooling: CMakeLists.txt
- Builds, bindings and tooling: wasm
- Builds, bindings and tooling: fuzzers
- Builds, bindings and tooling: cmake.yml
- Builds, bindings and tooling: README
- Moving from Lexbor, for the calls side by side.
- Roadmap, for when each planned row arrives.
- Conformance, for the test counts next to Lexbor's.
Parsec is Apache-2.0 and not yet released; these pages describe main. Found something wrong here? Open an issue or ask in Discussions.
Repository · Issues · Discussions · Roadmap · Security policy · Licence