LEPTRIS
lep · hare — tris · three

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

0×faster text parse than pugixml
0×faster than nokogiri, real docs
0/438W3C XPath 1.0 conformance
0/205XSLT byte-identical to libxslt
zerorequired runtime dependencies

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.

The full story of the motif →

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.

3.4× / text-heavy DOM

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.

1 arena per parse

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.

586 tests pin the semantics

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.

64 B element · 40 B attr

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.

11 SAX events, one scanner

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.

4 verbs — files or stdin

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)

leptris via binding18 ms
Ox 118 ms
Nokogiri libxml2123 ms

2 MB text-heavy DOM parse — vs pugixml, C harness 3.4× vs pugixml

median of interleaved runs, identical Release flags, same machine

leptris 193 µs
pugixml 663 µs

Write side — serialize, vs pugixml 1.5× vs pugixml

2 MB text-heavy and attr-heavy documents

leptris text 2 MB420 µs
pugixml text 2 MB650 µs
leptris attr-heavy449 µs
pugixml attr-heavy692 µs

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.

noderesidentwhy
element64 Bone cache line; self-relative int32 edges
attribute40 Bviews into the buffer; entity flag packed in the hash word
text node64 Bborrowed content until first expansion
comment / CDATA / PI48 Bcarved from the same arena block as elements
parse arena1 allocationretained 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 leptris

Ruby

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).

C99 engine

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.

Ruby gem

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.

Python

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.

C11 engine

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.

Ruby gem

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.

Python

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 .