Syntax highlighting - hand-written single-pass lexers
fuz_code (@fuzdev/fuz_code) is a runtime syntax highlighting library optimized
for HTML generation with CSS classes. It originated as a fork and by-hand rewrite of PrismJS,
with a redesigned tokenizer that replaces regular expressions with lexers per
language emitting a flat token event stream.
For coding conventions, see Skill(fuz-stack).
git add and git commit are denied by .claude/settings.local.json in
this repo — make the edits and stop, the user commits.
gro check # typecheck, test, lint, format check (run before committing)
gro typecheck # typecheck only (faster iteration)
gro test # run tests with vitest
gro gen # regenerate .gen files
gro build # build the package for production
gro src/test/fixtures/update # regenerate test fixturesIMPORTANT for AI agents: Do NOT run gro dev - the developer will manage the
dev server.
- Svelte 5 - component framework (optional peer dep, for Code.svelte)
- fuz_css (@fuzdev/fuz_css) - CSS variables for theming (optional peer dep)
- esm-env -
DEVflag (required peer) - magic-string, zimmerframe - build-time preprocessor helpers (
dependencies;svelte_preprocess_fuz_codeonly) - fuz_util (@fuzdev/fuz_util) - preprocessor helper (required peer)
- fuz_ui (@fuzdev/fuz_ui) - docs system (dev only)
fuz_code is a syntax highlighting library:
- Runtime HTML generation with CSS classes
- Hand-written single-pass lexers (zero regex), one per language
- 9 built-in lexers (
markup,xml,svelte,md,ts,css,json,sh,rust) plus the no-opplaintextevery styler registers - Extensible by writing a lexer (
SyntaxLang) - Optional Svelte component (
Code.svelte) and a build-time Svelte preprocessor (svelte_preprocess_fuz_code) that pre-renders staticCodecontent
- A build-time/SSR-only highlighting architecture (use Shiki) — the optional
preprocessor pre-renders static
<Code content="…">at build time, but the library's model is runtime lexing - TextMate grammar compatibility — some edge cases differ from IDE highlighting
- VS Code theme support
- Line numbers or code editing
- Syntax validation or error detection
benchmark/ # performance testing
├── run_benchmarks.ts # CLI entry (--save writes results.md + baseline.json)
├── benchmarks.ts # benchmark cases, output metrics, markdown writers
└── compare/ # Prism/Shiki comparison
src/
├── lib/ # exportable library code
│ ├── syntax_styler.ts # SyntaxStyler class: registry + lex/stylize facade
│ ├── syntax_styler_global.ts # pre-configured global instance
│ ├── lexer.ts # lexer substrate: Lexer, TokenTypeRegistry, flat events, HTML render
│ ├── lexer_*.ts # hand-written lexers (json, ts, css, bash, markup, svelte, md, rust)
│ ├── Code.svelte # main Svelte component
│ ├── svelte_preprocess_fuz_code.ts # build-time preprocessor for static `Code` content
│ ├── code_sample.ts # `CodeSample` shape + `sample_langs` for the demo site
│ ├── CodeHighlight.svelte # experimental CSS Highlight API
│ ├── CodeTextarea.svelte # experimental live-highlighted textarea
│ ├── highlight_manager.ts # CSS Highlight API manager
│ ├── range_highlighting.svelte.ts # shared range-highlighting helper
│ ├── highlight_priorities.ts # generated token priorities
│ ├── theme.css # token CSS classes
│ ├── theme_variables.css # CSS variable fallbacks
│ └── theme_highlight.css # CSS Highlight API theme
├── test/ # test files and fixtures
│ ├── highlight_manager.test.ts
│ ├── highlight_test_helpers.ts
│ ├── syntax_styler.test.ts # registry/facade behavior
│ ├── svelte_preprocess_fuz_code.test.ts
│ ├── lexer*.test.ts # lexer-engine suites (substrate + per language)
│ ├── lexer.pathological.test.ts # linearity + validity on adversarial inputs
│ ├── pathological.ts # pathological input generators (tests + benchmark)
│ └── fixtures/
│ ├── samples/ # source of truth sample files
│ ├── generated/ # generated fixture outputs
│ ├── helpers.ts # sample discovery + html/debug-text generation
│ ├── check.test.ts # fixture validation
│ └── update.task.ts # fixture regeneration task
└── routes/ # demo/docs site
├── samples/ # shared sample data (all.gen.ts)
├── benchmark/ # interactive benchmark UI
├── lang_color.ts # per-language tint for the docs language buttons
├── library.ts # svelte-docinfo library metadata for the API docs
└── docs/ # tomes: usage, samples, textarea, benchmark, api
Lexer engine (lexer.ts + lexer_*.ts) - hand-written single-pass
lexers emitting flat token events (Int32Array) rendered to HTML in one
forward pass. Token types intern into a TokenTypeRegistry
(token_types_global by default, injectable via SyntaxStylerOptions).
SyntaxStyler - The main class: a language registry and a lex/stylize
facade over the lexer engine. lex(text, lang) returns the flat event stream
(LexedSyntax); stylize(text, lang) renders it to HTML. Every instance
registers plaintext, a no-op lexer whose text renders escaped but unstyled —
the explicit "highlight nothing" language, distinct from an unregistered one
(which throws).
syntax_styler_global - Pre-configured instance with all built-in languages registered. Import this for typical usage.
svelte_preprocess_fuz_code - Build-time Svelte preprocessor that walks the
component AST for Code usages with a statically known content (plain
strings and ternary chains, resolved through module-level bindings) and
rewrites them to dangerous_raw_html with the HTML already generated. Skips
usages with spreads, a custom syntax_styler, or a non-static lang, so it
degrades to runtime highlighting rather than failing.
The lexer emits a flat event stream (LexedSyntax) — leaf/open/close records
in one Int32Array with interned type ids; plain text is implicit between
events (recovered from offsets). render_syntax_html streams HTML from it in
one forward pass, wrapping spans with classes like .token_keyword,
.token_string (styled by theme.css); syntax_events_to_tokens flattens it
to {type, start, end} for tests and fixtures.
One SyntaxLang lexer per language, registered via add_lang (ids and
aliases in the Supported languages table below):
lexer_json.ts- JSON (with comments)lexer_ts.ts- TypeScript, also servingjs/javascript— TS is a syntactic superset, so there is no separate JS lexerlexer_css.ts- CSS (including native nesting)lexer_bash.ts- the bash-family shell lexer (POSIX sh is a syntactic subset for highlighting — bash-family only, no fish etc.)lexer_markup.ts- HTML (rawtext script/style/textarea/title,style=/on*=attribute embedding) and XML (plain tag scanning), one shared scanner parameterized byMarkupLexModelexer_svelte.ts- Svelte: the markup scanner in svelte mode (script→ts, no special attrs, js-style comments between attributes) plus the{…}expression lexer (blocks, each/await splits, at-directives, directive modifiers,{const …}/{let …}declaration tags)lexer_md.ts- Markdown: line-oriented block scan (fences with exact-word info matching and any-length closers, headings, blockquotes, lists, hr) with a per-block inline scan (emphasis, inline code, links, entities, raw markup via the markup scanner); fences embed their languageslexer_rust.ts- Rust: one flat scan loop (nested block comments and raw strings resolve with counters; attribute interiors lex inline under an[/]depth counter — no frame machine), lifetime-vs-char'disambiguation,name!macros, doc-vs-plain comment split, the r/b/c string prefixes
Embedded languages resolve lazily by name through the registry (markdown
fences → any language, markup <script>/<style>/style=/on*=, svelte
{…} → ts) via Lexer.embed, which bounds nesting (MAX_EMBED_DEPTH) so
the one cycle in the embed graph — markdown fences embedding markdown —
can't overflow the call stack; past the cap a region stays plain text.
highlight_priorities.gen.ts→highlight_priorities.ts- extracts::highlight(token_name)rules fromtheme_highlight.cssand generates theHighlightTokenNametype plus per-token priorities (rule order = priority)src/routes/samples/all.gen.ts→all.ts- inlines the fixture samples asCodeSamplerecords for the demo site and the internal benchmark
SyntaxStyler class:
stylize(text, lang)- generate HTML with syntax highlightinglex(text, lang)- lex to the flat token event stream (LexedSyntax)add_lang(lang)- register aSyntaxLanglexer (and its aliases)has_lang(id)- whether a language is registered underidlangs- the id→lexerMap, keyed by primary id and every aliasSyntaxStylerOptions.token_types- inject a separateTokenTypeRegistry(only valid when every registered lexer interned into that same registry)
Both lex and stylize throw on an unregistered language — guard with
has_lang, or use plaintext to render unstyled.
Lexer substrate (lexer.ts) - the authoring and consumer surface beneath
the styler:
lex_syntax(text, lang, langs?, types?)/render_syntax_html(lexed)- the two halvesSyntaxStylercomposessyntax_events_to_tokens(lexed)- flatten events to{type, start, end}(fixtures, tests, range building)validate_syntax_events(lexed)- structural invariants as a list of issues, empty when valid (used by the pathological suite)token_type(name, alias?)/TokenTypeRegistry/token_types_global- interning;words_map(...entries)- keyword tablesLexer- the lex context:text/pos/end, theleaf/open/closeemitters, andembed(lang_id, start, end)- scanning helpers -
is_space,is_digit,is_upper,is_ascii_alnum,is_ascii_word,is_hex_digit,is_ident_start,is_ident,scan_ident,skip_space,trim_space_end,scan_to_line_end,skip_quoted,matches_ci,advance_probe,scan_balanced_braces
Code.svelte props:
content- source code to highlightdangerous_raw_html- pre-highlighted HTML (what the preprocessor emits); mutually exclusive withcontentand skips runtime highlightinglang- language identifier (default: 'svelte';nullor an unregistered id disables highlighting and renders plain text, warning in DEV)inline- boolean for inline vs blockwrap- boolean for text wrappingnomargin- boolean for margin controlsyntax_styler- customSyntaxStyler(default:syntax_styler_global)children- optional snippet receiving the generated HTML string, to customize rendering; remaining props spread onto the<code>element
Primary ids and their aliases, as registered in syntax_styler_global:
| id | aliases | lexer |
|---|---|---|
markup |
html, mathml, svg |
lexer_markup.ts |
xml |
ssml, atom, rss |
lexer_markup.ts |
svelte |
— | lexer_svelte.ts |
md |
markdown |
lexer_md.ts |
ts |
typescript, js, javascript |
lexer_ts.ts |
css |
— | lexer_css.ts |
json |
— | lexer_json.ts |
sh |
bash, shell |
lexer_bash.ts |
rust |
rs |
lexer_rust.ts |
plaintext |
— | syntax_styler.ts |
- Edit samples in
src/test/fixtures/samples/sample_*.{lang} - Run
gro src/test/fixtures/update(ornpm run fixtures:update) to regenerate — it invokesgenfirst, so the samples also refreshsrc/routes/samples/all.tsfor the docs site and benchmark - Run
gro test src/test/fixtures/checkto verify - Review changes with
git diff src/test/fixtures/
Generated fixtures in generated/{lang}/ include .html (tokenized output) and
.txt (debug output with token names).
npm run benchmark # internal benchmark: stdout + baseline comparison
npm run benchmark:save # …and update benchmark/results.md + benchmark/baseline.json
npm run benchmark:clean # delete benchmark/baseline.json (re-seed on next --save)
npm run benchmark:vs # compare against Prism and Shiki (stdout only)
npm run benchmark:vs:write # …and update benchmark/compare/results.mdThe internal benchmark covers all sample files at normal and 100x sizes plus a
pathological: group of generated adversarial inputs
(src/test/pathological.ts, 32KB each, shorter per-case budget) that tracks
the engine's worst-case constants — the same generators back the CI linearity
suite (src/test/lexer.pathological.test.ts). Each run also reports a
deterministic "Output metrics" table (spans rendered and utf8 HTML bytes per
case — DOM-cost proxies, not timed). Every run compares against
benchmark/baseline.json via @fuzdev/fuz_util's
benchmark_baseline_compare; only --save mutates it.
:vs (not :compare) marks the cross-implementation shootout against Prism
and Shiki (JS and Oniguruma engines), as opposed to the in-tree baseline
compare. Its language coverage is fixed at ts, css, html, json,
svelte by supported_languages in benchmark/compare/compare.ts — md,
sh, and rust are not in it.
benchmark/compare/results.md is linked from README.md as evidence for the
"about two orders of magnitude faster" claim vs Shiki — load-bearing for the
public narrative, not incidental output. benchmark/results.md is the local
perf baseline. benchmark/baseline.json is its machine-readable counterpart
and is gitignored (per the fuz_util/fuz_ui convention) — perf numbers vary
across machines, so it's a per-developer tracker, re-seeded per machine; PR
review relies on the committed results.md. --save writes the doc artifact
and the baseline together so they can't drift apart.
benchmark:vs:writefully overwritesbenchmark/compare/results.md— single-source, every byte fromformat_comparison_results, no baseline.benchmark:saverewrites the region between<!-- node-bench:start -->and<!-- node-bench:end -->inbenchmark/results.md(removing the sentinels makes--saveerror with a recovery hint rather than guess), plusbenchmark/baseline.json(10% regression threshold, 30-day staleness warning). The baseline compare runs before the save, so the run still reports what changed against the prior baseline.
The ## Browser Benchmark Results section of benchmark/results.md is
hand-pasted from the browser benchmark UI (src/routes/benchmark/) and
preserved untouched — its inputs only exist client-side, so there's no
auto-write path.
A stale baseline.json is auto-deleted with a warning when fuz_util's
baseline schema version advances or the JSON is corrupt — re-seed with
npm run benchmark:save.
CodeHighlight.svelte supports the CSS Custom Highlight API for native browser
highlighting. Limited browser support - use Code.svelte for production.
modeprop: 'auto', 'ranges', or 'html'CodeTextarea.svelte- editable<textarea>with live range highlightingHighlightManagerclass manages highlights per elementtheme_highlight.cssprovides both.token_*classes and::highlight()pseudo-elements
Limitations: no font-weight/font-style support in range mode.
theme.css and theme_highlight.css reference the fuz_css palette at the 50
stop — --text_50 and --color_a_50 through --color_j_50 (--color_c_50 is
currently unused):
--text_50- punctuation, doctype, cdata, processing instructions,=in attributes--color_a_50- keywords, tags, constants, symbols, booleans, null, headings--color_b_50- comments, char literals, blockquotes, inserted--color_d_50- at-keywords, urls--color_e_50- selectors, functions, regex, variables, important--color_f_50- atrules--color_g_50- special keywords, namespaces, rules--color_h_50- strings, attribute values, inline code--color_i_50- attribute names, properties, decorators, link text--color_j_50- builtins, class names, numbers
Rust's added token types ride existing colors via aliases — lifetime→symbol,
macro→function, attribute→attr_name, doc_comment→comment — so no
theme rules were added for them.
theme_variables.css (the fallback for consumers not using fuz_css) declares
the older --color_a_5 / --text_color_5 spelling and so does not currently
satisfy either theme.
- Zero regex in lexers - char-code scanning, native
indexOf, keywordMaplookups; noRegExpanywhere (speed + Rust-twin discipline) - Never throw, always cover - any input (mid-keystroke, malformed) yields a valid event stream; unterminated constructs extend to their natural boundary (damage propagates editor-style rather than being contained — a malformed interior may consume its construct's closing delimiter)
- Progress discipline - every scan loop advances position or emits+advances
- Nesting discipline - no JS-call-stack recursion that scales with input
nesting. Flat single-pass loops wherever the grammar allows (css, markup,
svelte, json, md, and rust need no stacks at all); an explicit pooled frame
machine only where a construct's interior re-enters the same grammar
(
lexer_ts.tstemplates/generics/annotations,lexer_bash.tssubstitutions — use these as the template, including their inline fast paths for bodies without nesting). An interior frame discovers its own closing delimiter during the real scan (a terminator char + depth counter on the frame) — never prescan for it: a prescan is a second, dumber tokenizer that can disagree with the real one, and it makes full-depth nesting quadratic. Cross-language interiors go throughLexer.embed, which is depth-capped rather than framed. - Test with fixtures - all changes must pass fixture tests
- Follow patterns - use existing
lexer_*.tsmodules as templates
New languages are written as lexers:
- Create
src/lib/lexer_{lang}.tsexporting aSyntaxLang, per the development guidelines above - Register via
add_langinsyntax_styler_global.ts - Style any new token types in
theme.cssandtheme_highlight.css, or give them an alias onto an existing type (token_type('macro', 'function')) — the rust lexer aliases all four of its additions rather than adding rules - Add samples in
src/test/fixtures/samples/sample_{variant}.{lang} - Wire the new file extension into both sample-discovery filters — the
file_filterregexes insrc/routes/samples/all.gen.tsandsrc/test/fixtures/helpers.tsare separate hardcoded copies, and a sample the filter misses is silently skipped rather than failing - Add the lang to
sample_langsincode_sample.ts(its order drives the docs) and a tint insrc/routes/lang_color.ts— that's aRecord<SampleLang, string>, so a missing entry fails typecheck - Add it to
languagesinsrc/routes/benchmark/benchmark_fixtures.ts— the browser benchmark uses a fixed list, unlike the internal benchmark which auto-discovers samples - Add a
src/test/lexer_{lang}.test.tssuite (targeted cases + prefix-resilience + determinism, mirroring the existing suites) - Generate fixtures and test
/docs- tomes: usage, samples, textarea, benchmark, api/benchmark- interactive browser benchmark (work vs paint timing)
Skill(fuz-stack) covers the shared conventions. Repo-specific:
- Prettier with tabs, 100 char width (not tsv)
- Node >= 24.14
fuz_css- semantic-first CSS framework (provides color variables)fuz_ui- UI componentsfuz_template- starter template using fuz_code