Zig-native implementation of the Unicode Bidirectional Algorithm (UAX #9).
Codepoint-based API inspired by FriBidi's design, implemented idiomatically in Zig with no global state and explicit allocator passing. Competitive with FriBidi and ICU; faster on LTR and mixed-direction text, with scratch APIs for allocation-light high-throughput loops.
- Status — implemented rules and roadmap
- API — quick reference with code examples
- Usage Guide — choosing between owned and scratch APIs, memory ownership, terminal integration
- Benchmarks — performance comparison vs zabadi, fribidi, ICU
- Compatibility — conformance results and differential testing
- Building — build commands and dependencies
- Examples — runnable example
| Rule | Description | Status |
|---|---|---|
| P2-P3 | Paragraph direction detection | Done |
| X1-X8 | Explicit embeddings, overrides, isolates | Done |
| X9 | Remove explicit codes (marked BN) | Done |
| W1-W7 | Weak type resolution | Done |
| N0 | Bracket pair resolution | Done (BD16 pairing scoped per IRS) |
| N1-N2 | Neutral type resolution | Done |
| I1-I2 | Implicit level resolution | Done |
| L1 | Reset segment/paragraph separators | Done (parts 1-3) |
| L2 | Reorder by level | Done |
| Rule | Description |
|---|---|
| L3 | Combining mark (NSM) reordering |
| L4 | Mirroring (data table ready, application pending) |
These are cosmetic refinements that don't affect terminal integration — terminal shapers (HarfBuzz, CoreText) handle combining marks and mirroring at the glyph level.
- L3/L4 completion — combining mark reordering and mirroring application for non-shaper consumers
- Arabic joining (R1-R7) — currently out of scope; handled by shaping engines (HarfBuzz, CoreText) in terminal/GUI contexts
- Arabic shaping (presentation forms) — useful for environments without a shaper
- C ABI wrapper — stable binary interface for non-Zig consumers
- Full Unicode conformance:
BidiTest failed=0,BidiCharacterTest failed=0 - FriBidi parity:
11/12 PASS(known delta: strict BD16 IRS-local pairing in one synthetic case) - Details: docs/compatibility.md | docs/benchmarks.md
itijah follows UAX #9 BD16 with IRS-scoped bracket pairing. Pair candidates are collected independently per Isolating Run Sequence. This can diverge from FriBidi on rare synthetic isolate/bracket mixes. Validated by dedicated regression tests and full Unicode conformance.
const itijah = @import("itijah");
// One-shot: allocates per call, caller frees via deinit(allocator)
var dir: itijah.ParDirection = .auto_ltr;
var emb = try itijah.getParEmbeddingLevels(allocator, codepoints, &dir);
defer emb.deinit(allocator);
var vis = try itijah.reorderLine(allocator, codepoints, emb.levels, dir.toLevel());
defer vis.deinit(allocator);Fast preflight for terminal/editor rows:
if (!itijah.hasStrongRtl(row_codepoints)) {
// Host renderer can keep its normal LTR path.
}Scratch APIs reuse buffers across calls — zero allocations after warmup:
// Create once, reuse across frames/lines
var scratch = itijah.VisualLayoutScratch{};
defer scratch.deinit(allocator);
// Per-line in render loop (zero allocs after warmup)
const layout = try itijah.resolveVisualLayoutScratch(allocator, &scratch, codepoints, .{
.base_dir = .ltr,
});
// layout.levels, layout.runs, layout.l_to_v, layout.v_to_l — scratch-owned viewsFor the full API surface, ownership model, and integration guide, see docs/usage.md.
Minimal row pipeline (left-anchored terminal row, line-scoped bidi):
var bidi_scratch = itijah.VisualLayoutScratch{}; // create once, reuse across frames
defer bidi_scratch.deinit(allocator);
const layout = try itijah.resolveVisualLayoutScratch(allocator, &bidi_scratch, row_codepoints, .{
.base_dir = .ltr,
});
for (layout.runs) |run| {
// Feed shaper in logical order for this run's contiguous logical slice.
const logical_start = run.logical_start;
const logical_end = run.logical_start + run.len;
var logical_i = logical_start;
while (logical_i < logical_end) : (logical_i += 1) {
// Cluster is visual offset inside the run (terminal-critical mapping).
const cluster = itijah.clusterForLogical(run, logical_i);
shape_input.append(.{
.cp = row_codepoints[logical_i],
.cluster = cluster,
});
}
// After shaping, place glyphs by visual x for this run.
}itijah is currently a Zig source package API (not a stable C ABI).
- Public Zig APIs are intended to be maintainable and allocator-explicit.
- A stable binary ABI across Zig compiler versions is not guaranteed.
- If you need a stable C ABI boundary, expose a dedicated C wrapper layer and version it explicitly.
For now, keep API usage pinned to a tagged release and the documented minimum Zig version.
zig build # build library
zig build test # run tests
zig build bench # run itijah benchmark report
zig build bench-compare # compare itijah vs fribidi vs ICU
zig build test-diff # differential harness vs fribidi + ICU (deterministic generator)See CONTRIBUTING.md for branch policy, required checks, release gate, and performance workflow.
Requires Zig 0.16.0+.
- uucode v0.2.0 — Unicode property lookups
- UCD — conformance test data (lazy, test-only)
- For
bench-compare:- system FriBidi headers + library
- ICU runtime library (
icuuc) available on host (loaded dynamically)
- For
test-diff:- system FriBidi headers + library
- ICU runtime library (
icuuc) available on host (loaded dynamically)
If a host app already creates a uucode module (for example to avoid duplicate module instances in a mono-build), depend on itijah with:
const itijah_dep = b.dependency("itijah", .{
.target = target,
.optimize = optimize,
.shared_uucode = true,
});
const itijah_mod = itijah_dep.module("itijah");
itijah_mod.addImport("uucode", shared_uucode_mod);When shared_uucode = true, itijah exports the library module without creating its own uucode import, so the caller can inject a shared one.
For standalone development in this repository, run build/test commands without that flag.
macOS (Homebrew):
brew install fribidi icu4cDetailed tables, environment, and methodology: docs/benchmarks.md.
bench-compare measures itijah vs zabadi, fribidi, and ICU across analysis and reorder_line for LTR/RTL/MIXED corpora at 16, 64, 256, 512, 1024, with optional huge sizes 262144, 524288, and 1048576.
zig build bench # itijah-only report
zig build bench-compare # itijah vs zabadi vs fribidi vs ICU
ITIJAH_COMPARE_INCLUDE_HUGE=1 zig build bench-compare # include 262K-1M sizesBenchmark sources (bench/) are not exported in package .paths — run from source checkout.
zig build test # filtered (fast CI mode)
ITIJAH_CONFORMANCE_MODE=full zig build test # full Unicode conformance
zig build test-diff # differential vs fribidi + ICUFull conformance: BidiTest failed=0, BidiCharacterTest failed=0.
See docs/compatibility.md for parity status and differential harness details.
MIT