Performance First
Identifier and width queries are single-digit nanoseconds; normalization is dominated by its one output allocation and short-circuits when the input is already normalized. Latest local Criterion means (cargo bench --bench bench, Windows x86_64, Rust stable, release build):
| Operation | Time |
|---|---|
is_xid_start / is_xid_continue |
~3.2 ns |
is_xid (11-scalar identifier) |
~37 ns |
char_width (per scalar) |
~5.2 ns |
str_width (mixed 48-column string) |
~288 ns |
normalize (ASCII, already normalized) |
~33 ns |
normalize (mixed scripts → NFC) |
~128 ns |
is_normalized (ASCII → NFC) |
~13 ns |
Numbers vary by CPU and environment; run the suite on your target to establish a baseline. Data is generated from Unicode 16.0.0 (UNICODE_VERSION).
Features
- Identifier rules —
is_xid_start,is_xid_continue, and the whole-stringis_xidimplement the UAX #31XIDprofile a lexer needs. - Normalization — all four forms (NFC, NFD, NFKC, NFKD) through one
normalize, plus a fastis_normalizedquick-check. Verified against the official conformance suite. - Display width —
char_widthandstr_widthgive monospace column counts (0 / 1 / 2),wcwidth-style. - No dependencies — tables are generated from the UCD and embedded; nothing is pulled in at build time.
no_std— identifier and width queries are allocation-free; normalization is gated behindalloc(on by default).- Fully safe —
#![forbid(unsafe_code)]. - Property-tested — algebraic laws (idempotence, form composition, width additivity) checked across randomized inputs with
proptest.
Installation
[]
= "1"
# no_std without normalization (identifier + width only, no allocator):
= { = "1", = false }
# Serde support for the `Form` selector:
= { = "1", = ["serde"] }
MSRV is 1.85+ (Rust 2024 edition).
Quick Start
use ;
// Recognise an identifier that mixes scripts.
assert!;
assert!; // cannot start with a digit
// Normalize: compose a base letter and a combining accent.
assert_eq!;
// Compatibility folding: a ligature and a fullwidth digit.
assert_eq!;
assert_eq!;
// Measure display width for column alignment.
assert_eq!;
assert_eq!; // wide CJK ideograph
Lexing identifiers
The two per-scalar predicates are what a hand-written lexer calls in its inner loop:
use ;
assert_eq!;
assert_eq!;
assert_eq!;
How it works
The public API is three small modules over one generated data file:
- Identifiers consult the
XID_StartandXID_Continueranges fromDerivedCoreProperties. A query is one binary search over sorted, disjoint code-point ranges. - Width classifies a scalar as zero-width (controls, combining marks, format characters, conjoining Hangul jamo), wide (East Asian Wide / Fullwidth), or the default one column.
- Normalization decomposes each scalar through the full canonical or compatibility mapping, reorders combining marks by canonical class, and — for NFC / NFKC — recomposes via the primary-composite table. Hangul is handled by formula. An already-normalized string is detected by the quick-check and returned untouched.
The tables are produced by dev/gen_tables.rs, a committed, dependency-free generator that reads the UCD text files and emits src/tables.rs. Regenerating against a newer Unicode release is a one-command step; the shipped crate contains only the generated data.
API Overview
For the complete reference with examples, see docs/API.md.
is_xid_start/is_xid_continue/is_xid— UAX #31 identifier rules.char_width/str_width— monospace display width.normalize/is_normalized/Form— the four normalization forms.UNICODE_VERSION— the UCD version the tables were built from.
Feature Flags
| Feature | Default | Description |
|---|---|---|
std |
✅ | Enables alloc. The crate uses no other std facilities. |
alloc |
✅ | Gates the allocating normalize / is_normalized API. Off ⇒ identifiers + width only. |
serde |
❌ | Serialize / Deserialize for the Form selector. |
Testing
The property suite in tests/proptests.rs checks the algebraic laws — normalization is idempotent and stable, the forms compose as UAX #15 requires, and width is additive. tests/conformance.rs always runs a curated set of hard cases, and runs the entire official NormalizationTest.txt suite whenever the UCD data is present locally or in CI.
Cross-Platform Support
The crate is pure table lookups with no platform-specific code, so it behaves identically everywhere Rust runs. CI covers Linux, macOS, and Windows on both stable and the 1.85 MSRV; the full conformance suite is validated on Windows and Linux (WSL2 Ubuntu).
Contributing
See REPS.md for the engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.