Fast as hares.
Contained as a circle.
libleptris parses XML 1.0, evaluates all of XPath 1.0, and streams SAX — in pure C99, with one arena per document and a memory ceiling you can state before you parse.
- Speed you can measure — vectorized scans, one contiguous arena, an open benchmark harness.
- Memory you can promise — every allocation bounded at parse time, released in a single call.
- Nothing behind the ring — zero required dependencies; the same core from C, Ruby, Python, Rust, or the shell.
speed in the chase — containment in the circle
each hare two ears — three among them
The name
Three hares, three ears
On the ceilings of the Dunhuang caves, and centuries later on carved roof bosses in parish churches across Europe, the same image appears:three hares chasing in a circle, sharing three ears — each hare seems to have two, yet there are only three among them.
That is the contract of this library. Speed: hares in full chase — vectorized scans, one contiguous arena, indices where others walk. Containment: the circle — every allocation bounded at parse time, a hard memory ceiling, a self-sufficient core with nothing hidden behind the ring.
What it is
A parser built like a system, not a script
Every design choice optimizes for the same pair: throughput you can measure, and resource behavior you can promise.
Speed, measured honestly
Interleaved A/B gates against pugixml and libxml2 on every shape — text-heavy, attr-heavy, deep, pretty, markup-rich. Median-of-runs, fresh build dirs, the harness published alongside the numbers. Serialize is 1.5–1.6× ahead of pugixml; the XPath engine serves repeated //x from a subtree-interval index at effectively no cost.
Hard resource bounds
The parser pre-sizes one contiguous arena from the document itself — element, attribute, text and markup blocks carved from a single allocation, then retained for reuse. No hidden per-node mallocs, no unbounded growth mid-parse.
Conformant where it counts
XPath 1.0: 438/438 on the W3C suite. XSLT:205/205 byte-identical to libxslt. HTML: the html5lib tree suite, rising every release. XML 1.0 namespaces, DTD validation phases 1–7, XInclude, exclusive C14N, RELAX NG and Schematron behind one validate — semantics pinned by the corpora plus large-document and allocation-failure suites in CI.
Compact by construction
One cache line per element and per attribute; compact self-relative edges; zero-copy views into the document buffer. A 4 MB document parses into roughly 3 MB of tree.
DOM and SAX, one core
Streaming SAX with all eleven events beats libxml2’s SAX on every measured shape — same scanner, same bounds, no DOM required.
Four bindings, one core
gem leptris, pip leptris and theleptris Rust crate wrap the C surface — GC-safe handles in Ruby, Drop-safe wrappers in Rust. The 4.38 MB real-world document: 18 ms parse+serialize through Ruby vs Nokogiri’s 123 ms.
A CLI that respects your pipe
leptris parse | xpath | format — MECE output modes (XML / JSON / text), reads files or stdin, man pages included. Not a demo binary: the same public API, nothing private.
The race
Numbers from the open harness
Every figure below reproduces from benchmarks/matrix in the repository — min/median over interleaved runs, fresh Release builds, medians reported because library minimums wobble under CI load.
Real-world document, 4.38 MB — parse + serialize, through Ruby 6.8× vs nokogiri
10,000 records, pretty-printed, encoding-declared; same process, same harness (serialbench)
2 MB text-heavy DOM parse — vs pugixml, C harness 3.4× vs pugixml
median of interleaved runs, identical Release flags, same machine
Write side — serialize, vs pugixml 1.5× vs pugixml
2 MB text-heavy and attr-heavy documents
Shapes where pugixml still leads (attr-heavy parse ~1.5×, its duplicate-allowing attribute API ~1.6× on raw append) are documented with their measured floors in the repository’s perf ledger — nothing is cherry-picked, and the harness that produced every number is in the tree.
The full race, per language — every library we compare against →
The circle
Memory you can promise
The arena is sized from the document before the first node is carved; mutation storage grows in chained blocks owned by the document. What you allocate is what you hold — stated up front, released all at once.
| node | resident | why |
|---|---|---|
| element | 64 B | one cache line; self-relative int32 edges |
| attribute | 40 B | views into the buffer; entity flag packed in the hash word |
| text node | 64 B | borrowed content until first expansion |
| comment / CDATA / PI | 48 B | carved from the same arena block as elements |
| parse arena | 1 allocation | retained and reused across documents |
The surface
Small API, everywhere
One C99 core; the same handles and the same bounds from C, Ruby, Python, or the shell.
#include <leptris.h>
LeptrisStatus st;
LeptrisDocument* doc = leptris_parse_string(xml, len, &st);
LeptrisElement root = leptris_document_root(doc);
for (LeptrisElement e = leptris_element_first_child(root, "record");
e; e = leptris_element_next_sibling(e, "record")) {
const char* id = leptris_element_attribute(e, "id");
}
char* out = leptris_document_serialize(doc, NULL); /* caller frees */
leptris_free_string(out);
leptris_document_free(doc); /* arena, all at once */The chase begins
Install, four ways
C99 — builds anywhere CMake does. Optional utf8proc and iconv; neither is required. Current release: libleptris v1.9.156, Ruby gem v1.9.156.0.
C — CMake
# vcpkg, or FetchContent: include(FetchContent) FetchContent_Declare(leptris GIT_REPOSITORY https://github.com/leptris/leptris) FetchContent_MakeAvailable(leptris) target_link_libraries(yours PRIVATE leptris::leptris)
C — vcpkg
# manifest mode: add to vcpkg.json
{ "dependencies": [ "leptris" ] }
# or classic:
vcpkg install leptrisRuby
gem install leptris
require "leptris"
doc = Leptris::XML.parse("<greet>hello</greet>")
doc.to_xml # => "<greet>hello</greet>"Python
pip install leptris
from leptris import fromstring
root = fromstring("<greet>hello</greet>")
print(root.tag) # => "greet"Full build matrix, link flags, and per-language setup:the getting-started guide →
The family — TOML
teptris: TOML at library speed
The TOML sibling of libleptris — a parser and writer in pure C99, zero required dependencies, stable ABI. 714/714 on BurntSushi’s toml-test; the 4×-mandate gate shapes confirmed on developer hardware (mixed 4.13×, cargo 4.67×, deep 4.97×, table 5.84× against the best C/C++ competitor).
libteptris →
TOML 1.0 + the 1.1 draft, canonical emit, zero-copy views, batch parsing, and a schema-descriptor ABI — withpkg-config and a teptris:: CMake export on cmake --install.
teptris →
tomlib-shaped: datetimes become Time/Date, batch and lazy paths, schema plans — fat platform gems with one extension per Ruby minor, 3.0–4.0.
teptris →
tomllib-shaped: loads/dumps with the tomllib error contract, loads_batch,loads_lazy — self-contained abi3wheels for CPython ≥ 3.9.
Meet teptris — the dedicated section → · The TOML race, per shape →
The family — YAML
yeptris: YAML at library speed
The YAML sibling of libleptris — a YAML 1.2 parser, writer and streamer in pure C11, zero required dependencies, stable ABI. 395/395 on the yaml-test-suite, 279 libyaml emission goldens at zero divergences, up to 3.36× libyaml same-binary — and the packed record tape as the default parse route, so parse-only workloads never build nodes.
libyeptris →
YAML 1.2 with a libyaml/Psych-compatible 1.1 lenient mode, DOM / push / pull / record-tape consumption models over one engine, zero-copy views, plans and schemas — withpkg-config on cmake --install.
yeptris →
Psych-shaped: YAML 1.1 typing by default,schema: :core_12 for 1.2 core, a strict-JSON twin, the Document node API — FFI platform gems, 14 release legs.
yeptris →
PyYAML-shaped: safe_load/safe_dumpwith PyYAML’s SafeLoader typing — ctypes FFI, one bulk drain per document, self-contained abi3 wheels.
Meet yeptris — the dedicated section → · The YAML race, per shape →
Fast as hares.
Contained as a circle.
Clone it, build it, race it against your current parser — the harness is in the tree.
git clone https://github.com/leptris/leptris && cmake -B build -S .