LEPTRIS

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-24LeptrisParseOptions.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) parsingleptris_pull_new/_next/_free over the streaming SAX core: host-driven events with zero C→host callbacks; memory bounded by the input slice, not the document.
  • Iterparse, boundedleptris_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 XPathleptris_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 optionsLeptrisParseOptions + 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.

Full changelog in the repo →


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-step find("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 after close()), and find() disagreed with findall() 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=1 forces 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 packleptris_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 reportingleptris_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 via leptris_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 accessorsleptris_xpath_result_get_nodes_ex copies every mixed-nodeset entry with its kind; leptris_element_children bulk-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_API declaration (196 == 196); header/binary drift now fails the build.
  • Namespace sets from pairsleptris_xpath_ns_set_new_from_pairs: one FFI call for the whole prefix/URI array.

Full changelog in the repo →


libleptris v1.2.0

2026-08-23 — the C core.

  • Namespace-bound XPathLeptrisXPathNsSet + leptris_xpath_eval_ns: prefixed name tests resolve by namespace, matching elements that carry it via any prefix or the default namespace. XPointer xmlns(prefix=uri) components now bind subsequent xpointer() bodies instead of being skipped.
  • Spec fixprefix:* 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 emit prefix:name so namespace bindings survive a reparse.
  • Programmatic rootsleptris_element_create’s mutation-block fast path skipped root→doc registration, and leptris_document_free had 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.

Full changelog in the repo →


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 RubyDocument#exslt enables the first-party str:/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_cleanup bound for worker-thread exit.
  • One-call namespace bindings — namespace-bound XPath builds its set in a single FFI call via the pairs API; NodeSet#each fetches 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 — serialized name="value" form with the five XML special characters escaped.
  • FixesNode#text dispatches to #content so subclass overrides are seen; Element#namespace carries 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 bulk leptris_element_children fill from 4 children up (2× on wide elements); nodeset materialization via ffi.unpack (13–18% off the wrapper loop); XPath errors prefer the document-scoped leptris_document_last_error.
  • v1.4.1 — engine-side subtree walks. iter()/iterdescendants() delegate to one C-speed descendant evaluation 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 via leptris_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; Element with Clark-notation tag, text/tail, read-only attrib, the iter family; find/findall accept full XPath 1.0; xpath takes namespaces and variables. 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 | //item on 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 with Drop, status→Result mapping, 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.

Full changelog in the repo →


libleptris v1.1.1

2026-08-23 — the conformance sprint.

  • One unified tag space for mixed XPath nodesets (#477) — node_kind/node_name/node_value agree 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_node for all node kinds.
  • Since 1.1.1Element#each_attribute (O(n) vs the O(n²) index re-walk), Element#prefix, and precompiled platform gems: gem install leptris works 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.