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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
src/test/fixtures/generated/**
src/test/fixtures/samples/sample_*.*
src/test/fixtures/diff/**
67 changes: 62 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ dev server.
- esm-env - `DEV` flag (required peer)
- magic-string, zimmerframe - build-time preprocessor helpers (`dependencies`;
`svelte_preprocess_fuz_code` only)
- fuz_util (@fuzdev/fuz_util) - preprocessor helper (required peer)
- fuz_util (@fuzdev/fuz_util) - preprocessor helper + diff data for the diff
viewer (required peer)
- fuz_ui (@fuzdev/fuz_ui) - docs system (dev only)

## Scope
Expand All @@ -50,6 +51,8 @@ fuz_code is a **syntax highlighting library**:
- Optional Svelte component (`Code.svelte`) and a build-time Svelte
preprocessor (`svelte_preprocess_fuz_code`) that pre-renders static `Code`
content
- Syntax-highlighted diff rendering, unified and side-by-side
(`CodeDiff.svelte`/`CodeDiffSplit.svelte` over `diff_html.ts`)

### What fuz_code does NOT include

Expand All @@ -76,7 +79,10 @@ src/
│ ├── 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)
│ ├── diff_html.ts # diff viewer: unified + split rendering over fuz_util's diff data
│ ├── Code.svelte # main Svelte component
│ ├── CodeDiff.svelte # diff viewer component (unified view)
│ ├── CodeDiffSplit.svelte # diff viewer component (side-by-side view)
│ ├── 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
Expand All @@ -85,28 +91,33 @@ src/
│ ├── range_highlighting.svelte.ts # shared range-highlighting helper
│ ├── highlight_priorities.ts # generated token priorities
│ ├── theme.css # token CSS classes
│ ├── theme_diff.css # diff viewer theme (row tints via fuz_css intent variables)
│ ├── 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
│ ├── html_test_helpers.ts # HTML assertion helpers (diff + line rendering)
│ ├── syntax_styler.test.ts # registry/facade behavior
│ ├── svelte_preprocess_fuz_code.test.ts
│ ├── diff_html.test.ts # diff rendering, both views
│ ├── lexer*.test.ts # lexer-engine suites (substrate + per language)
│ ├── lexer.html_lines.test.ts # per-line rendering + marks
│ ├── lexer.pathological.test.ts # linearity + validity on adversarial inputs
│ ├── pathological.ts # pathological input generators (tests + benchmark)
│ └── fixtures/
│ ├── samples/ # source of truth sample files
│ ├── diff/ # diff case dirs, each an a/b source pair
│ ├── generated/ # generated fixture outputs
│ ├── helpers.ts # sample discovery + html/debug-text generation
│ ├── helpers.ts # sample + diff-case 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
└── docs/ # tomes: usage, samples, diff, textarea, benchmark, api
```

### Core system
Expand Down Expand Up @@ -139,9 +150,33 @@ 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
`.token_string` (styled by `theme.css`); `render_syntax_html_lines` renders
one balanced fragment per source line (spans open at a newline close there
and reopen on the next line), with optional `marks` ranges wrapped in
`<mark>` tags — marks wrap text runs only, cut at token boundaries, so
nesting always stays valid; `syntax_events_to_tokens` flattens it
to `{type, start, end}` for tests and fixtures.

### Diff viewer

`diff_html.ts` renders syntax-highlighted unified diffs:
`render_diff_unified_html(a, b, options)` composes `@fuzdev/fuz_util/diff.ts`
(Myers line diff, hunks, intra-line segments) with whole-document lexing per
side and `render_syntax_html_lines`. Rows are semantic `<ins>`/`<del>`
(context rows are `<span>`) carrying `diff_line` + `diff_add`/`diff_remove`/
`diff_same`, with aria-hidden unselectable gutters, `+`/`-` markers as CSS
generated content (copied text stays clean code), intra-line `<mark>`
emphasis, and elided unchanged regions as zero-JS `<details>` blocks
(options: `'details' | 'omit' | 'none'`). `render_diff_split_html` is the
side-by-side sibling — a flat cell sequence for a two-column grid, pairing
k-th remove with k-th add and padding unpaired sides with `.diff_spacer`
cells. `CodeDiff.svelte`/`CodeDiffSplit.svelte` are the thin wrappers owning
the `<div class="code_diff">`/`<div class="code_diff_split">` elements
(split defaults `wrap` on — half-width panes); `theme_diff.css` (opt-in,
alongside `theme.css`) styles both, with row tints keyed off fuz_css intent
variables (`--positive_*`/`--negative_*`) through `--diff_*` custom
properties.

### Language definitions

One `SyntaxLang` lexer per language, registered via `add_lang` (ids and
Expand Down Expand Up @@ -231,6 +266,22 @@ the styler:
- `children` - optional snippet receiving the generated HTML string, to
customize rendering; remaining props spread onto the `<code>` element

**CodeDiff.svelte / CodeDiffSplit.svelte props** (unified / side-by-side):

- `a`, `b` - the original and updated source texts (or `dangerous_raw_html`
like `Code.svelte`)
- `lang` - language identifier (default: 'svelte'; `null` renders plain rows
with diff chrome only)
- `context_lines` - unchanged lines around changes (default: 3)
- `elide` - `'details' | 'omit' | 'none'` for unchanged regions (default:
'details')
- `intraline` - intra-line `<mark>` emphasis on paired lines (default: true)
- `line_numbers` - gutters (default: true)
- `max_cost` - cost cap for the line diff, forwarded to fuz_util's
`diff_lines` (see `DiffOptions.max_cost` in `@fuzdev/fuz_util/diff.ts`)
- `wrap`, `nomargin`, `syntax_styler` - as in `Code.svelte` (`wrap` defaults
on for the split view)

## Supported languages

Primary ids and their aliases, as registered in `syntax_styler_global`:
Expand Down Expand Up @@ -262,6 +313,12 @@ Primary ids and their aliases, as registered in `syntax_styler_global`:
Generated fixtures in `generated/{lang}/` include `.html` (tokenized output) and
`.txt` (debug output with token names).

Diff fixtures follow the same flow with pairs: each `src/test/fixtures/diff/{case}/`
holds an `a.{lang}` + `b.{lang}` source pair (byte-exact — the whole dir is
prettierignored, since trailing newlines and whitespace are diff inputs), and
`generated/diff/{case}` gets `.html` (unified), `.split.html`, and `.txt`
(stats + plain unified-diff text).

## Performance

```bash
Expand Down Expand Up @@ -408,7 +465,7 @@ New languages are written as lexers:

## Demo pages

- `/docs` - tomes: usage, samples, textarea, benchmark, api
- `/docs` - tomes: usage, samples, diff, textarea, benchmark, api
- `/benchmark` - interactive browser benchmark (work vs paint timing)

## Project standards
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@fuzdev/blake3_wasm": "^0.1.1",
"@fuzdev/fuz_css": "^0.63.2",
"@fuzdev/fuz_ui": "^0.206.6",
"@fuzdev/fuz_util": "^0.65.1",
"@fuzdev/fuz_util": "^0.67.0",
"@fuzdev/gro": "^0.205.1",
"@fuzdev/mdz": "^0.3.0",
"@ryanatkn/eslint-config": "^0.12.1",
Expand Down
140 changes: 140 additions & 0 deletions src/lib/CodeDiff.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { DEV } from 'esm-env';
import type { SvelteHTMLElements } from 'svelte/elements';

import { syntax_styler_global } from './syntax_styler_global.ts';
import type { SyntaxStyler } from './syntax_styler.ts';
import { render_diff_unified_html, type RenderDiffOptions } from './diff_html.ts';

const {
a,
b,
dangerous_raw_html,
lang = 'svelte',
context_lines = 3,
elide = 'details',
intraline = true,
line_numbers = true,
max_cost,
wrap = false,
nomargin = false,
syntax_styler = syntax_styler_global,
children,
...rest
}: SvelteHTMLElements['div'] &
(
| {
/** The original version of the source. */
a: string;
/** The updated version of the source. */
b: string;
dangerous_raw_html?: undefined;
}
| {
a?: undefined;
b?: undefined;
/**
* Pre-rendered diff rows, bypassing runtime diffing and highlighting.
* Named `dangerous_raw_html` to signal that it bypasses sanitization,
* matching the `{@html}` pattern used by `Code.svelte`.
*/
dangerous_raw_html: string;
}
) & {
/**
* Language identifier for syntax highlighting. `null` disables
* highlighting (rows render as plain text with diff chrome);
* `undefined` falls back to the default ('svelte').
*
* @default 'svelte'
*/
lang?: string | null;
/**
* Unchanged lines of context around changes.
*
* @default 3
*/
context_lines?: number;
/**
* How elided unchanged regions render — see `RenderDiffOptions.elide`.
*
* @default 'details'
*/
elide?: RenderDiffOptions['elide'];
/**
* Whether paired remove/add lines get intra-line emphasis.
*
* @default true
*/
intraline?: boolean;
/**
* Whether to render line-number gutters.
*
* @default true
*/
line_numbers?: boolean;
/**
* Cost cap for the line diff — see `RenderDiffOptions.max_cost`.
*/
max_cost?: number;
/**
* Whether to wrap long lines (`pre-wrap` instead of `pre`).
*
* @default false
*/
wrap?: boolean;
/**
* Whether to disable the default margin-bottom.
*
* @default false
*/
nomargin?: boolean;
/**
* Custom `SyntaxStyler` instance.
*
* @default syntax_styler_global
*/
syntax_styler?: SyntaxStyler;
/**
* Optional snippet to customize how the diff rows are rendered.
* Receives the generated HTML string as a parameter.
*/
children?: Snippet<[markup: string]>;
} = $props();

// DEV-only validation warnings
if (DEV) {
$effect(() => {
if (dangerous_raw_html != null) return;

if (lang && !syntax_styler.has_lang(lang)) {
const langs = [...syntax_styler.langs.keys()].join(', ');
// eslint-disable-next-line no-console
console.error(
`[CodeDiff] Language "${lang}" is not supported. ` +
`Rows render as plain text. Supported: ${langs}`
);
}
});
}

const html_content = $derived.by(() => {
if (dangerous_raw_html != null) return dangerous_raw_html;
return render_diff_unified_html(a, b, {
lang,
syntax_styler,
context_lines,
elide,
intraline,
line_numbers,
max_cost
});
});
</script>

<!-- eslint-disable svelte/no-at-html-tags -->

<div {...rest} class={['code_diff', rest.class]} class:wrap class:nomargin data-lang={lang}>
{#if children}{@render children(html_content)}{:else}{@html html_content}{/if}
</div>
Loading