Changelog
Two components, one core
The C library and the Ruby gem release on their own cadence, in lockstep when the gem rides a core release. This page carries the headlines; the repositories carry the full history.
libleptris v1.9.3 – v1.9.145 — the September engines
2026-09-12 — four engines landed on one core, each gated by an external corpus: XSLT closed to 205/205 byte-identical against the libxslt suite (v1.9.19) and grew the 2.0/3.0 working set (tunnel params, maps/arrays, accumulators, xsl:evaluate/merge/result-document, character maps, inline + higher-order functions), with the in-process scorecard vs libxslt now measured fixture-by-fixture (predicate dispatch 1.7× and subtree copy 2.05× ahead, value-of at parity, dispatch shapes behind — the full table and its ceiling analysis on the benchmarks page); XQuery (FLWOR, constructors, try/catch, external variables) with W3C QT3 function sets adopted exact-N; HTML as two entries — the WHATWG tree-construction engine (adoption agency, formatting reconstruction, script-data escaped states, RCDATA family, foreign integration points; html5lib corpus 933 → 1169 across v1.9.137–.143, entities01.dat fully green; numeric-reference end states in v1.9.143) and the Nokogiri-parity entry byte-pinned to a 1555-case reference; and validation without a JVM — native RELAX NG (Jing-parity gate, <include>, params) plus native Schematron (SVRL, abstract patterns, phases, corpus 50/50) behind one leptris validate CLI, with a native XML diff lane alongside. XPath stayed 438/438. v1.9.144 adds leptris_element_expanded_name — local name, prefix, and resolved namespace URI through one call, for FFI adapters that fan out per-element reads. v1.9.145 is a maintenance release: the public version macros are corrected and kept in sync by the release tooling, the XSLT scorecard gains committed in-process libxslt reference twins, and the benchmark matrix harness reports the library versions it actually links. Full per-release notes in the GitHub releases.
leptris gem v1.9.121.0 — the lockstep line
2026-09-09 — the 1.9 lockstep line rides the September engines: the RELAX NG arc completes at v1.9.115.0 (validator behind the Jing conformance gate 38/38, <include> with Jing’s override rules, <param> datatype facets); v1.9.121.0 adopts WHATWG MathML/SVG foreign content, in-table wrapper synthesis, and the insertion-mode edges (audit 270/270 symbols against the C surface). Every gem release from 1.9.86 onward pins its engine and passes the lockstep audit in CI.
leptris (Python) v1.9.145.0 — the lockstep line
2026-09-09 — the binding joins the lockstep numbering: WHATWG adoption agency through foreign content and table wrappers (v1.9.107 → v1.9.121), the #919 C14N attribute-order fix, Document.doctype, and leptris.RelaxNG (v1.9.115.0) — native RELAX NG validation bound, no JVM in the process. The line continues in lockstep with every engine release through v1.9.145.0 on PyPI.
libleptris v1.9.0
2026-08-24 — LeptrisParseOptions.recover (#547): parse failures return an empty document with the error recorded. Rootless documents with PIs serialize correctly instead of returning "" (#546). serialize_into takes options and caches: size-query + into-call reuse one serialization (#541).
leptris gem v1.9.x — both worlds memoize
2026-08-27 — the 1.9 line adopts the engine’s 1.7–1.9 surface (expanded-name attribute access, per-attribute namespaces, recover parsing, caller-buffer serialization) and lands the performance arc: writable documents memoize through a mutation-version cache (namespace inspection 29×, children 11× — readonly parity), readonly loops 13–36× over 1.9.2, single-node queries (at_xpath/at_css) on a container-free seam (up to 3.6×), batch materialization type-hinted, SAX strings UTF-8, fragments searchable with receiver-relative CSS, and a lifetime contract (UseAfterFreeError) enforced on every borrowed handle. Lockstep audit CI-gated; rake audit:symbols ships in the gem.
libleptris v1.7.0
2026-08-24 — the two FFI-ergonomics APIs from the Ox-vs-leptris audit (#535): leptris_node_children copies every child kind in one call (out=NULL counts all kinds), and the _serialize_into pair writes into caller buffers — one call, zero library-side allocations.
libleptris v1.6.2
2026-08-24 — pretty-printing no longer inserts whitespace inside mixed-content elements (#534); pretty round-trips are byte-stable.
libleptris v1.6.0
2026-08-24 — all six user-filed issues fixed; the issue queue is empty. Same-parent node moves no longer corrupt the sibling chain (the moxml-adapter hang, #518); union nodesets keep attribute identity instead of dangling pointers (#514); detached PI/comment/CDATA mutations work on rootless documents (#519); document-level processing instructions gained a public API (#526).
libleptris v1.5.1
2026-08-24 — two correctness fixes: element serialization emitted every following sibling (52× faster single-element serialize, and correct output for the first time, #523), and unprefixed XPath name tests no longer match namespaced elements — XPath 1.0 §2.3 parity with libxml2/Nokogiri/REXML (#525).
libleptris v1.5.0
2026-08-24 — the TODO.engine board. File-backed streaming: leptris_pull_new_file / leptris_iterparse_new_file stream documents off disk in bounded slices — huge files parse end to end with no whole-document buffer. Compiled XPath grew its context variants (leptris_xpath_compiled_eval_ns / _eval_vars), so compiled handles work everywhere the namespace- and variable-bound evaluators did. The Rust crate gained a publish workflow and rides engine lockstep.
libleptris v1.4.0
2026-08-23 — the C core. Current release: the bindings-parity drop.
- Pull (StAX-style) parsing —
leptris_pull_new/_next/_freeover the streaming SAX core: host-driven events with zero C→host callbacks; memory bounded by the input slice, not the document. - Iterparse, bounded —
leptris_iterparse_new/_next/_free: each top-level child materializes in its own pool and is released on the next yield; peak memory bounded by the largest subtree, not the document. - Compiled XPath —
leptris_xpath_compile/_compiled_eval/_compiled_free: one parse for a handle’s lifetime, skipping the per-call cache probe; the handle is immutable, so concurrent evaluation from many threads is safe. - Per-parse options —
LeptrisParseOptions+leptris_parse_string_ex: scoped strict/depth/flags with thread defaults restored on return. - Plus truthful serialization encoding declarations. Binding-side adoption of the new APIs follows.
leptris (Python) v1.14.3 — the engine room, and the walls bracketed
2026-08-27 — ten releases in four days; the binding grew an engine room.
- v1.9.0 — iterparse + compiled XPath, and single-mode.
leptris.iterparse(source)streams top-level children with memory bounded by the largest subtree;leptris.XPath(expr)compiles once and evaluates many times (contexts, namespaces);Document.parse(recover=True)yields a rootless document instead of raising;Element.get({"{uri}local"})resolves Clark-notation attributes. The pure-Python fallback is gone — the accelerator is required. - v1.12.0 — subtree walks in C.
iter()/iterdescendants()became a C cursor stepping first-child/next-sibling/parent directly — no expression built, no engine evaluation, bounded memory. The XPath engine now serves queries only; child iteration is one C call returning a presized list. - v1.13.x — the Document joins the raw-address pattern. Parse + root-promote + registry creation collapsed into one C call; serialization and
find()gained C fast paths (multi-stepfind("a/b")is a first-match DFS — 6.5× faster than the list-materializing path it replaced); the seventeen Python accessors the C type shadowed were deleted — every accessor has exactly one home. - v1.14.x — compiled XPath rejoins the fast path; libleptris 1.9.2. The compiled class had kept the old Python conversion path (34.8 µs vs lxml’s 8.3 for a namespaced query) — one C call through the shared converter brings it to parity with the plain path. Adopts libleptris 1.9.2 (the options-struct ABI fix; 1.9.1 is warned off). Three correctness fixes along the way: SAX handler reuse raised stale errors,
Node.as_element()bypassed the poisoning registry (a use-after-free afterclose()), andfind()disagreed withfindall()on namespaced documents. - The walls, bracketed. Every measured operation shape (18 of them, plus memory, nesting depth, and import time) is at parity or ahead of lxml. The three remaining losses are engine-side, measured and filed: iterparse’s fixed per-call cost (leptris/leptris#563), namespaced XPath evaluation at ~18× the un-namespaced path (leptris/leptris#564), and variable-bound evaluation at ~79× the literal equivalent (leptris/leptris#565). Peak RSS runs ~35% below lxml’s.
Full changelog in leptris-py →
leptris (Python) v1.6.1 — every operation beats lxml
2026-08-23 — the accelerator release.
- v1.6.0 — the C accelerator. Cost accounting showed the entire remaining gap to lxml was Python object construction (~0.10 µs per Element). A ~180-line abi3 accelerator (
_leptrisaccel) moved Element allocation to C — 0.026 µs each — while the whole API surface stays in Python, attached onto the C heap type. Wheels ship it compiled; without a toolchain the package degrades to the equivalent pure-Python class (LEPTRIS_PURE=1forces it). - Final scoreboard vs lxml 6.0.2 (macOS arm64, py3.10, libleptris 1.3.0): parse 8×,
count()4.7×, ID predicates 10×, price predicates 5.3×, serialize 3.7×,//book(100) 1.9× win, union (200) 1.5× win, traversal (401) 18.0 vs 21.6 µs — 1.2× win. Every operation. - v1.6.1 — five wheels + sdist on PyPI. cp39-abi3 wheels for macOS arm64/x86_64, manylinux x86_64/aarch64, and Windows — the manylinux aarch64 wheel builds natively on an arm runner (1.6.0’s emulated build had silently skipped it). pip-resolvable, accelerator confirmed inside every wheel.
Full changelog in leptris-py →
libleptris v1.3.0
2026-08-23 — the C core: the concurrency and EXSLT drop.
- EXSLT extension pack —
leptris_exslt_enable(doc)activates 15 native handlers:str:replace/tokenize/split/concat/padding,set:distinct/intersection/difference/leading/trailing,math:max/min/abs/sqrt/power. - Thread-safe error reporting —
leptris_last_error()is thread-local; parse failures record a message with byte offset; failing XPath evaluations snapshot into a per-document slot (leptris_document_last_error). Parse-error positions vialeptris_last_error_position— XMLSyntaxError/Nokogiri parity. - Threading contract documented — one-document-per-thread needs no locking, read-only sharing is safe; the XPath AST/bytecode cache is mutex-guarded and pin-counted (fixing a real use-after-free under concurrent first evaluation);
leptris_thread_cleanup()drains per-thread caches. The full suite is ThreadSanitizer-clean. - Batch FFI accessors —
leptris_xpath_result_get_nodes_excopies every mixed-nodeset entry with its kind;leptris_element_childrenbulk-fills child elements — closing the per-item dispatch gap to lxml/libxml2 in bindings. - Export-surface gate — CI and ctest diff the shared library’s exports against every
LEPTRIS_APIdeclaration (196 == 196); header/binary drift now fails the build. - Namespace sets from pairs —
leptris_xpath_ns_set_new_from_pairs: one FFI call for the whole prefix/URI array.
libleptris v1.2.0
2026-08-23 — the C core.
- Namespace-bound XPath —
LeptrisXPathNsSet+leptris_xpath_eval_ns: prefixed name tests resolve by namespace, matching elements that carry it via any prefix or the default namespace. XPointerxmlns(prefix=uri)components now bind subsequentxpointer()bodies instead of being skipped. - Spec fix —
prefix:*name tests were namespace-blind everywhere (//t:*matched every element in the document); now namespace-scoped. - Serializer round-trip — CDATA sections containing
]]>split across two sections (the libxml2 technique); element and attribute names emitprefix:nameso namespace bindings survive a reparse. - Programmatic roots —
leptris_element_create’s mutation-block fast path skipped root→doc registration, andleptris_document_freehad a use-after-free with programmatic roots (found via the moxml adapter contract, lutaml/moxml#96). - Releases — fully automated release PRs: changelog drafted from conventional commits, notes embedded in the PR body.
leptris gem v1.4.0
2026-08-23 — the Ruby binding, in lockstep with libleptris v1.4.0. Binding-side adoption of the pull/iterparse/compiled-XPath faces follows.
Full changelog in leptris-ruby →
leptris gem v1.3.0
2026-08-23 — the Ruby binding, in lockstep with libleptris v1.3.0.
- EXSLT in Ruby —
Document#exsltenables the first-partystr:/set:/math:function pack as native C handlers on that document. Document#last_error— per-document error retrieval, the thread-safe successor to the library-global string;leptris_thread_cleanupbound for worker-thread exit.- One-call namespace bindings — namespace-bound XPath builds its set in a single FFI call via the pairs API;
NodeSet#eachfetches mixed-kind results through the batch accessor (the per-index fallback is gone).
Full changelog in leptris-ruby →
leptris gem v1.2.0
2026-08-23 — the Ruby binding, in lockstep with libleptris v1.2.0.
Leptris::XML::Document.create— an empty document with its own memory pool; build from scratch, no sentinel parse.Document#root=— attach a programmatic root (companion to the moxml Leptris adapter).Attr#to_xml— serializedname="value"form with the five XML special characters escaped.- Fixes —
Node#textdispatches to#contentso subclass overrides are seen;Element#namespacecarries the element’s own prefix. Pins libleptris v1.2.0, whose fixes close leptris#477.
Full changelog in leptris-ruby →
leptris (Python) v1.5.0
2026-08-27 — the first Python release: an lxml-shaped API.
- v1.5.0 — adopts libleptris 1.3.0.
Element.sourceline(lxml parity); child iteration via the bulkleptris_element_childrenfill from 4 children up (2× on wide elements); nodeset materialization viaffi.unpack(13–18% off the wrapper loop); XPath errors prefer the document-scopedleptris_document_last_error. - v1.4.1 — engine-side subtree walks.
iter()/iterdescendants()delegate to one C-speeddescendantevaluation plus batch materialization: traversal 291 → 58 µs vs lxml’s 21 (was 14× behind, now 2.7×). - v1.4.0 — batch nodesets.
//book(100 results) 30.4 → 16.4 µs, union 63.4 → 34.4 µs vialeptris_xpath_result_get_nodes;leptris.libleptris_version()reports the loaded library at runtime. - v1.3.0 — breaking, by design. The API mirrors lxml (the Ruby binding mirrors Nokogiri, so the Python binding mirrors lxml):
fromstring/XML/parse/tostring/c14n;Elementwith Clark-notationtag,text/tail, read-onlyattrib, theiterfamily;find/findallaccept full XPath 1.0;xpathtakesnamespacesandvariables. New:leptris.sax— one-shot and a constant-memory streaming push parser. The interim 1.2.0 API shipped to no adopters and is replaced outright; the package follows its own semver. - v1.3.1 — tuned against the benchmark matrix. Nodeset materialization ~1.6×, traversal −45% (element-level sibling chains,
__slots__), single-FFI sibling accessors.
Full changelog in leptris-py →
libleptris v1.1.2
2026-08-23 — the C core.
- XPath union dedup O(n²) → O(n) —
//name | //itemon a 20k-element document: 229 ms → 1.6 ms (~140× faster); multi-context steps dedup at any size. - Element-index build hashed — documents with many distinct values (20k unique ids) paid a one-time 300 ms spike on the second query; all three bucket lookups are hashed now (~75×).
- Spec fix —
//node()now selects the root element per XPath 1.0. leptris_error_message/leptris_last_error— declared since the first release but never implemented (phantom symbols); both are implemented and exported now.- Rust bindings (crate
leptris) — safe Document/Element wrappers withDrop, status→Resultmapping, mixed-nodeset XPath accessors, SAX with closures; CI on ubuntu/macos/windows. The roadmap board is now empty. - pyleptris releases — the PyPI publish trigger now fires from the release flow; the Python binding’s tests run on all three platforms per PR.
libleptris v1.1.1
2026-08-23 — the conformance sprint.
- One unified tag space for mixed XPath nodesets (#477) —
node_kind/node_name/node_valueagree on every entry; synthetic attribute/namespace/text tags no longer collide with DOM node kinds. - True document order for nodeset results (#485); reverse axes (ancestor/preceding families) return reverse document order per the spec;
//text()~7× faster via single-pass pre-order walks. - Predicate semantics — bare
text()/node()in expression position fixed (a[text()='x']now matches); string-values for text/CDATA/comment/PI nodes. - aarch64 — NEON text scan compiles on GCC (glibc and Alpine musl) and MSVC ARM64 (#487).
leptris gem v1.1.2
2026-08-22 — the Ruby binding, in lockstep with libleptris v1.1.2.
- v1.1.2 — bindings for the full v1.1.0+ public surface: attribute iteration, the mixed-nodeset API, element prefixes; NodeSets fetch via
get_nodefor all node kinds. - Since 1.1.1 —
Element#each_attribute(O(n) vs the O(n²) index re-walk),Element#prefix, and precompiled platform gems:gem install leptrisworks on x86_64/aarch64-linux (incl. musl), mingw/ucrt Windows, and x86_64/arm64 macOS — no system library, no env vars. - Releases — a fully automated pipeline builds the 8-platform matrix (plus musl containers) and publishes via OIDC trusted publishing.
Full changelog in leptris-ruby →
Earlier releases — libleptris v1.1.0 and older, gem v1.1.0 and older — are in the canonical changelogs:libleptris ·leptris gem.