Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plain-peas-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuzdev/fuz_code': minor
---

make `sh` the primary shell language id, with `bash` and `shell` as aliases
44 changes: 20 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
> 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 PrismJS fork and keeps
its `.token_*` class vocabulary, but the tokenizer is a full rewrite — one
hand-written single-pass lexer per language emitting a flat token event stream,
with zero regular expressions.
for HTML generation with CSS classes. It originated as a PrismJS fork, but the
tokenizer is a full rewrite — one hand-written single-pass lexer per language
emitting a flat token event stream, without regular expressions.

For coding conventions, see Skill(fuz-stack).

Expand Down Expand Up @@ -73,7 +72,9 @@ src/
│ ├── lexer_*.ts # hand-written lexers (json, ts, css, bash, markup, svelte, md)
│ ├── Code.svelte # main Svelte component
│ ├── 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
Expand All @@ -89,9 +90,9 @@ src/
│ ├── check.test.ts # fixture validation
│ └── update.task.ts # fixture regeneration task
└── routes/ # demo/docs site
├── samples/ # language samples showcase
├── samples/ # shared sample data (all.gen.ts)
├── benchmark/ # interactive benchmark UI
└── docs/ # API documentation
└── docs/ # tomes: usage, samples, textarea, benchmark, api
```

### Core system
Expand Down Expand Up @@ -125,8 +126,9 @@ One `SyntaxLang` lexer per language, registered via `add_lang`:
- `lexer_ts.ts` - TypeScript; also registers the `js`/`javascript` aliases
(TS is a syntactic superset — there is no separate JS lexer)
- `lexer_css.ts` - CSS (including native nesting)
- `lexer_bash.ts` - Bash; also registers the `sh`/`shell` aliases (POSIX sh
is a syntactic subset for highlighting — bash-family only, no fish etc.)
- `lexer_bash.ts` - the bash-family shell lexer, registered as `sh` with
`bash`/`shell` as aliases (POSIX sh is a syntactic subset for highlighting —
bash-family only, no fish etc.)
- `lexer_markup.ts` - HTML (`markup`/`html`/`mathml`/`svg`: rawtext
script/style/textarea/title, `style=`/`on*=` attribute embedding) and XML
(`xml`/`ssml`/`atom`/`rss`: plain tag scanning), one shared scanner
Expand Down Expand Up @@ -162,15 +164,17 @@ can't overflow the call stack; past the cap a region stays plain text.

**Code.svelte props:**

- `content` (required) - source code to highlight
- `lang` - language identifier (default: 'svelte')
- `content` - source code to highlight (or pass `dangerous_raw_html` from the
preprocessor instead)
- `lang` - language identifier (default: 'svelte'; `null` disables highlighting)
- `inline` - boolean for inline vs block
- `wrap` - boolean for text wrapping
- `nomargin` - boolean for margin control
- `syntax_styler` - custom `SyntaxStyler` (default: `syntax_styler_global`)

## Supported languages

`ts`, `js`, `css`, `html`, `json`, `svelte`, `md`, `bash`
`ts`, `js`, `css`, `html`, `json`, `svelte`, `md`, `sh`

## Testing

Expand Down Expand Up @@ -214,24 +218,15 @@ engines).
### Updating committed result snapshots

`benchmark/compare/results.md` is linked from `README.md` as evidence for
the "vastly faster than Shiki" claim — it's load-bearing for the public
narrative, not just incidental output. `benchmark/results.md` is referenced
the "about two orders of magnitude faster" claim vs Shiki — it's load-bearing
for the public narrative, not just incidental output. `benchmark/results.md` is referenced
locally as a perf baseline. `benchmark/baseline.json` is the machine-readable
counterpart used by `benchmark_baseline_compare` for regression detection.
It's **gitignored** (per the fuz_util/fuz_ui convention) — perf numbers
vary across machines, so the baseline is a per-developer local tracker,
re-seeded on each machine that runs benchmarks. PR review still relies on
the committed `results.md`.

Workflow:

```bash
npm run benchmark # check current perf against your local baseline
npm run benchmark:save # accept the change: rewrites results.md + baseline.json
npm run benchmark:clean # nuke the local baseline (re-seed on next --save)
npm run benchmark:vs:write # full overwrite of benchmark/compare/results.md
```

`--save` is a single switch by design — accepting a perf change should
update both the doc artifact and the local regression baseline atomically,
so they can't drift apart.
Expand Down Expand Up @@ -269,6 +264,7 @@ If the baseline schema version in `@fuzdev/fuz_util` advances, a stale
highlighting. Limited browser support - use `Code.svelte` for production.

- `mode` prop: 'auto', 'ranges', or 'html'
- `CodeTextarea.svelte` - editable `<textarea>` with live range highlighting
- `HighlightManager` class manages highlights per element
- `theme_highlight.css` provides both `.token_*` classes and `::highlight()`
pseudo-elements
Expand Down Expand Up @@ -335,8 +331,8 @@ New languages are written as lexers:

## Demo pages

- `/samples` - code samples in all supported languages
- `/benchmark` - interactive performance testing
- `/docs` - tomes: usage, samples, textarea, benchmark, api
- `/benchmark` - interactive browser benchmark (work vs paint timing)

## Project standards

Expand Down
58 changes: 28 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,35 @@

`fuz_code` is a runtime syntax highlighter: it turns source code into HTML with
`.token_*` CSS classes, and knows nothing about the DOM. It originated as a fork
of [Prism](https://github.com/PrismJS/prism) ([prismjs.com](https://prismjs.com/))
and keeps its `.token_*` class vocabulary, but the tokenizer is now a full
rewrite — one hand-written single-pass lexer per language emitting a flat token
event stream, with zero regular expressions.
of [Prism](https://github.com/PrismJS/prism) ([prismjs.com](https://prismjs.com/)),
but the tokenizer is now a full rewrite — one hand-written single-pass lexer per
language emitting a flat token event stream, without regular expressions.

Highlights:

- a minimal, explicit API to generate stylized HTML — `stylize(code, lang)`
- stateless ES modules, instead of globals with side effects
- written in TypeScript, with zero runtime dependencies
- written in TypeScript, with no runtime dependencies
- eight built-in languages (see below), extensible by writing a lexer

Two optional integrations:

- optional builtin [Svelte](https://svelte.dev/) support with a
- builtin [Svelte](https://svelte.dev/) support with a
[Svelte lexer](src/lib/lexer_svelte.ts) and a
[Svelte component](src/lib/Code.svelte) for convenient usage.
- The [default theme](src/lib/theme.css) integrates
with my CSS library [fuz_css](https://github.com/fuzdev/fuz_css) for colors that adapt to the user's runtime `color-scheme` preference.
Non-fuz_css users should import [theme_variables.css](src/lib/theme_variables.css)
or otherwise define those variables.

Compared to [Shiki](https://github.com/shikijs/shiki), fuz_code is much lighter
and [vastly faster](./benchmark/compare/results.md) for runtime usage: it runs
hand-written single-pass lexers rather than the
[Svelte component](src/lib/Code.svelte)
- the [default theme](src/lib/theme.css) integrates with
[fuz_css](https://github.com/fuzdev/fuz_css) for colors that adapt to the
user's runtime `color-scheme` preference — without fuz_css, import
[theme_variables.css](src/lib/theme_variables.css) or otherwise define those
variables

Compared to [Shiki](https://github.com/shikijs/shiki), fuz_code is smaller and
[about two orders of magnitude faster](./benchmark/compare/results.md) for
runtime usage: it runs hand-written single-pass lexers rather than the
[Oniguruma regexp engine](https://shiki.matsu.io/guide/regex-engines) that
TextMate grammars require, and has zero runtime dependencies instead of 38. Shiki
TextMate grammars require, and has no runtime dependencies instead of 38. Shiki
targets build-time use and supports far more languages and themes — pick the
tool that fits; fuz_code is optimized for small, fast, runtime highlighting.
tool that fits; fuz_code is optimized for small, fast runtime highlighting.

## Usage

Expand Down Expand Up @@ -74,8 +74,8 @@ With SvelteKit:
import '@fuzdev/fuz_code/theme.css';
```

The primary themes (currently just [one](src/lib/theme.css)) have a dependency
on my CSS library [fuz_css](https://github.com/fuzdev/fuz_css)
The primary themes (currently just [one](src/lib/theme.css)) depend on
[fuz_css](https://github.com/fuzdev/fuz_css)
for [color-scheme](https://css.fuz.dev/docs/themes) awareness.
See the [fuz_css docs](https://css.fuz.dev/) for its usage.

Expand All @@ -98,7 +98,7 @@ import '@fuzdev/fuz_code/theme_variables.css';
- [@fuzdev/fuz_code/Code.svelte](src/lib/Code.svelte) -
Svelte component for syntax highlighting with HTML generation

I encourage you to poke around [`src/lib`](src/lib) if you're interested in using fuz_code.
See [`src/lib`](src/lib) for the full set of modules.

### Languages

Expand All @@ -111,7 +111,7 @@ Registered by default in `syntax_styler_global` — one hand-written lexer each:
- [`ts`](src/lib/lexer_ts.ts) (TypeScript — also serves `js`/`javascript`, a syntactic subset)
- [`css`](src/lib/lexer_css.ts)
- [`json`](src/lib/lexer_json.ts) (with comments — jsonc)
- [`bash`](src/lib/lexer_bash.ts) (`sh`/`shell`)
- [`sh`](src/lib/lexer_bash.ts) (POSIX/bash family — also serves `bash`/`shell`)

Add a language by writing a `SyntaxLang` lexer and registering it with
`add_lang` — see the existing `lexer_*.ts` modules.
Expand All @@ -123,11 +123,11 @@ Docs are a work in progress:
- this readme has basic usage instructions
- [CLAUDE.md](./CLAUDE.md) has more high-level docs including benchmarks
- [code.fuz.dev](https://code.fuz.dev/) has usage examples with the Svelte component
- [samples](https://code.fuz.dev/samples) on the website
- [samples](https://code.fuz.dev/docs/samples) on the website
(also see the [sample files](src/test/fixtures/samples/))
- [tests](src/test/)

Please open issues if you need any help.
Issues and questions are welcome.

## Experimental highlight support

Expand All @@ -136,11 +136,10 @@ For browsers that support the
fuz_code provides an experimental component that can use native browser highlighting
as an alternative to HTML generation.

This feature is experimental, browser support is limited,
and there can be subtle differences because some CSS like bold/italics are not supported.
(nor are font sizes and other layout-affecting styles, in case your theme uses those)
The standard `Code.svelte` component
using HTML generation is recommended for most use cases.
Browser support is limited, and the API can't apply layout-affecting styles —
bold, italics, font sizes — so themes that use them render differently.
The standard [Code.svelte](src/lib/Code.svelte) component using HTML generation
is recommended for most use cases.

```svelte
<script lang="ts">
Expand Down Expand Up @@ -179,7 +178,6 @@ originally forked from [Prism](https://github.com/PrismJS/prism)
with the Svelte support originally based on
[`prism-svelte`](https://github.com/pngwn/prism-svelte) by
[@pngwn](https://github.com/pngwn). The tokenizer has since been rewritten as
hand-written lexers, but the `.token_*` class vocabulary and the fork's lineage
remain.
hand-written lexers, but the fork's lineage remains.

[MIT](LICENSE)
32 changes: 27 additions & 5 deletions benchmark/compare/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ export const run_comparison_benchmark = async (
const content = getSampleContent(lang, large);
const size_label = large ? 'large' : 'small';

// Tokenization benchmarks (fuz_code and Prism only)
// Tokenization benchmarks — raw tokenizer, no HTML rendering, so the
// output-shape difference (CSS classes vs Shiki's inlined theme colors)
// drops out and this is the cleanest apples-to-apples view. Shiki's
// `codeToTokensBase` is its lowest-level public tokenizer (single theme);
// it resolves per-token colors but the grammar walk dominates, so it's a
// fair analog to fuz_code's `lex` and Prism's `tokenize`.
bench.add(`fuz_code_tokenize_${lang}_${size_label}`, () => {
syntax_styler_global.lex(content, fuz_lang);
});
Expand All @@ -123,6 +128,14 @@ export const run_comparison_benchmark = async (
});
}

bench.add(`shiki_js_tokenize_${lang}_${size_label}`, () => {
shiki_js.codeToTokensBase(content, {lang: shiki_lang, theme: 'nord'});
});

bench.add(`shiki_oniguruma_tokenize_${lang}_${size_label}`, () => {
shiki_oniguruma.codeToTokensBase(content, {lang: shiki_lang, theme: 'nord'});
});

// Stylization benchmarks (all implementations)
bench.add(`fuz_code_stylize_${lang}_${size_label}`, () => {
syntax_styler_global.stylize(content, fuz_lang);
Expand Down Expand Up @@ -159,10 +172,11 @@ export const run_comparison_benchmark = async (
* column headers like `vs fuz_code_stylize_ts_large`).
*
* Each filter narrows by name suffix (`_${lang}_${size}$`) plus an operation
* substring so e.g. the "ts tokenize small" group catches only
* `fuz_code_tokenize_ts_small` and `prism_tokenize_ts_small`, not the stylize
* variants. The trailing `$` matters — without it, "ts_small" would also match
* "ts_small_*" if such a name ever existed.
* substring so e.g. the "ts tokenize small" group catches
* `fuz_code_tokenize_ts_small`, `prism_tokenize_ts_small`, and the two
* `shiki_*_tokenize_ts_small` variants, not the stylize ones. The trailing `$`
* matters — without it, "ts_small" would also match "ts_small_*" if such a name
* ever existed.
*/
const build_groups = (languages: ReadonlyArray<SupportedLanguage>): Array<BenchmarkGroup> => {
const groups: Array<BenchmarkGroup> = [];
Expand Down Expand Up @@ -192,6 +206,14 @@ export const format_comparison_results = (results: Array<BenchmarkResult>): stri
'',
'Comparing fuz_code vs Prism vs Shiki across multiple languages and content sizes.',
'',
'`tokenize` rows compare raw tokenizers with no HTML rendering — the',
'shape-neutral, apples-to-apples view. `stylize` rows compare the HTML each',
'library actually ships: fuz_code and Prism emit CSS-class spans',
'(`<span class="token_x">`) styled by a static stylesheet, while Shiki has no',
'native class output and inlines resolved theme colors on every span plus a',
'`<pre>` wrapper — so its `stylize` cost includes per-token color work the',
'class-based libraries offload to CSS.',
'',
'## Results',
'',
benchmark_format_markdown_grouped(results, groups),
Expand Down
Loading
Loading