XPath 1.0 — complete, and quick
All 13 axes, 27 functions, 15 operators, full predicate syntax. 438/438 on the W3C XPath 1.0 conformance suite — served by a bytecode VM that beats libxml2 on every measured query.
The engine
Expressions compile to a bytecode VM: compile-once, evaluate-many dispatch with per-axis specialization, predicate fast paths, absolute-path fusion, a per-document element index with attribute buckets, fused axis+predicate opcodes, and memcpy fast paths for index-backed queries. Repeated //x queries are served from a subtree-interval index at effectively no cost.
Axes — all thirteen
| axis | shorthand | notes |
|---|---|---|
ancestor | excludes self | |
ancestor-or-self | ||
attribute | @attr | works on all nodes |
child | default | the default axis |
descendant | excludes self | |
descendant-or-self | // | fused with name tests (//foo) |
following | excludes descendants | |
following-sibling | ||
namespace | namespace nodes | |
parent | .. | |
preceding | excludes ancestors | |
preceding-sibling | ||
self | . |
All axes maintain document order as the spec requires. Node tests: name tests, *, ns:*,text(), comment(),processing-instruction() (with and without target),node().
Predicates and abbreviated syntax
- Position predicates —
[N](1-based),[last()],[position()=N] - Boolean predicates —
[@attr],[child] - Value predicates —
[@id='1'], with fast paths - Multiple sequential predicates —
[1][@id] - Complex predicates —
[position() mod 2 = 0]
Mixed nodesets
Results can mix element nodes and synthetic attribute nodes —//a/@x is consumable through the public result API (leptris_xpath_result_node_kind / get_node / node_name / node_value) without touching internals. Bindings expose this through the result-scoped kind quartet, never through raw node tags.
Namespace-bound evaluation
Since v1.2.0, expressions can carry external namespace bindings: leptris_xpath_eval_ns with aLeptrisXPathNsSet resolves prefixed name tests bynamespace — an element matches when it carries the bound namespace via any prefix or the default namespace, and//t:* selects only elements in t’s namespace (previously prefix wildcards were namespace-blind). XPointerxmlns(prefix=uri) components bind subsequentxpointer() bodies too.
Measured — vs libxml2
From benchmarks/xpath/bench_diagnostic on a ~5 KB catalog fixture, CPU time:
| query | leptris | libxml2 | advantage |
|---|---|---|---|
self::* (per-call floor) | 0.57 µs | 0.89 µs | 1.6× faster |
child::* | 0.71 µs | 0.94 µs | 1.3× faster |
attribute::id | 0.63 µs | 2.52 µs | 4.0× faster |
descendant::* | 0.72 µs | 0.96 µs | 1.3× faster |
//book | 0.55 µs | ~1 µs | 1.8× faster |
//* | 0.56 µs | ~1 µs | 1.8× faster |
count(//book[@id='b1']) | 1.13 µs | ~3 µs | 2.7× faster |
descendant::*[@id] | 0.77 µs | 1.02 µs | 1.3× faster |
/catalog | 0.53 µs | ~1 µs | 1.9× faster |
leptris leads on all ten XPath benchmarks. Release 1.1.2 made union deduplication ~140× faster on large merged sets (//name | //item on 20k elements: 229 ms → 1.6 ms), and 1.1.1 made //text() ~7× faster via single-pass pre-order walks that emit results directly in document order. The element index currently accelerates absolute paths and root-context descendant queries; extending it to relative-descendant queries is on the roadmap.
EXSLT — the extension pack
Since lib v1.3.0, leptris_exslt_enable(doc) activates15 native handlers on a document — first-party C implementations, not interpreted callbacks:
str:—replace,tokenize(character-set delimiters),split(whole-pattern substrings),concat,paddingset:—distinct,intersection,difference,leading,trailingmath:—max,min,abs,sqrt,power
From Ruby: doc.exslt then query as usual. The extension is per-document — enabling it costs nothing until an extension function is called.
Thread safety
One document per thread needs no locking; sharing documents read-only across threads is safe. The XPath AST/bytecode cache is mutex-guarded and pin-counted, and leptris_thread_cleanup()drains per-thread caches when worker threads exit. Error reporting is thread-local, with per-document snapshots for XPath failures — the full suite runs ThreadSanitizer-clean.
Using it
LeptrisXPathResult r = leptris_xpath_eval(doc, NULL, "//item[@price > 10]");
if (r) {
size_t n = leptris_xpath_result_count(r);
for (size_t i = 0; i < n; i++) {
LeptrisNodeRef node = leptris_xpath_result_node(r, i);
printf(" %s\n", leptris_node_name(node));
}
leptris_xpath_result_free(r);
}From Ruby: doc.xpath("//item"); from Python:doc.xpath("//item"); from the shell:leptris xpath doc.xml "//item". SeeRuby, Python, andCLI.
Where next
- The compliance ledger — per-feature status in leptris-ruby.
- Canonical XPath guide —
docs/guide/xpath-queries.md.
This is the curated guide. The repositories are canonical: when this page and the repo disagree, the repo wins. Full documentation lives with the source.