v7.15.0
Features
- add
HighlightStreamfor incrementally streamed code (51754f6) - add experimental CSS Custom Highlight engine for
HighlightEditable(6ed5775) - add
transformhook toCopyButton(d27dcd4, #447) - handle OSC, carriage returns, and more SGR in
parseAnsi(09dc980, #449)
Fixes
- make the
highlightaction idempotent and reversible (4475d08, #455) - coerce non-string
codebefore highlighting (3319f17, #456) - recompute
HighlightStylescope class on theme change (719a297, #454) - re-highlight
HighlightEditablewhen language changes (1ca043a, #451) - hand off
HighlightStyle's<style>tag when its owner unmounts (0833494) - modernize WGSL grammar for the current spec (9f1d164)
Performance
- reveal
Typewriterunits without per-tick retokenization (0db8217, #450) - repaint only changed lines in
HighlightEditable(3bff41e) - track selection only while
HighlightEditableis focused (51615d6) - store
HighlightEditablehistory as slice diffs (c094a18) - read
HighlightEditable's code viatextContent, notinnerText(20e280b)
HighlightStream
Rendering LLM chat output is the dominant new syntax-highlighting use case: code arrives in arbitrary-sized chunks, mid-token and mid-line, and you don't know the full content up front. HighlightStream is built for that — unlike Typewriter, which animates a complete, already-highlighted string and restarts whenever it changes, HighlightStream accepts a growing code buffer and re-highlights it as chunks arrive.
Append to code as chunks come in; set done once the stream ends.
<script>
import { HighlightStream } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
let code = "";
let done = false;
// Wire this up to your streaming source (fetch, WebSocket, SSE, ...).
async function stream() {
for (const chunk of chunks) {
code += chunk;
await new Promise((r) => setTimeout(r, 30));
}
done = true;
}
</script>
<svelte:head>
{@html github}
</svelte:head>
<HighlightStream language={typescript} {code} {done} />Multiple chunks appended within the same animation frame coalesce into a single highlight pass. Already-rendered lines are diffed and left untouched in the DOM; only lines whose content actually changed are repainted, so a fast-scrolling response only touches its changed suffix (typically the last line). A multi-line construct left open mid-stream — an unterminated template literal or block comment — re-tokenizes the lines it spans once the closing delimiter arrives, with no special-casing needed.
A blinking caret marks the end of output while !done; setting done hides it and performs one final full highlight, so the finished output matches what Highlight would render for the same code. on:done fires right after that final highlight. Customize the caret with the same --caret-width, --caret-height, --caret-gap, --caret-color, and --caret-blink variables as Typewriter.
Set autoScroll to keep the container pinned to the bottom as output grows — it stops auto-scrolling as soon as you scroll up, and resumes once you scroll back to the bottom.
<HighlightStream
language={typescript}
{code}
{done}
autoScroll
style="max-height: 20em; overflow-y: auto;"
/>This is O(buffer) per highlighted frame and DOM updates proportional to the changed lines — fine for chat-sized output up to a few thousand lines, not a virtualized log viewer.
Experimental: CSS Custom Highlight engine
engine="css-highlights" paints tokens with the CSS Custom Highlight API (CSS.highlights, ::highlight()) instead of wrapping them in <span>s. The editable <code> stays plain text (one <span> per line, reused from the default engine's line structure, but with no per-token spans inside), so a repaint never replaces the DOM the caret is sitting in.
Requires Chrome 105+, Safari 17.2+, or Firefox 140+. Where CSS.highlights is unavailable, HighlightEditable falls back to engine="dom" silently; check what actually ran with editor.resolvedEngine().
Pass theme (the same theme string you'd give HighlightStyle) to generate ::highlight() rules from it. Only color/background-color convert — ::highlight() doesn't support font-style/font-weight/text-decoration across browsers, so bold/italic scopes render in plain color.
theme only covers token colors — you still need HighlightStyle (or an injected stylesheet) for the base .hljs layout rules (padding, overflow, background), exactly as with the default engine.
<script>
import { HighlightEditable, HighlightStyle } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
let code = "const add = (a: number, b: number) => a + b";
</script>
<HighlightStyle theme={github}>
<HighlightEditable
language={typescript}
bind:code
engine="css-highlights"
theme={github}
/>
</HighlightStyle>CopyButton transform
Displayed code often carries decoration that shouldn't survive a copy — shell prompts, REPL markers, diff signs. Pass a transform function to strip it before the copy function runs; on:copy reports the transformed string, not the original code.
stripPrompts and stripDiffMarkers cover the common cases and are exported for reuse (compose them for your own transform when a block needs both):
<script>
import { CopyButton, stripPrompts } from "svelte-highlight";
const code = "$ npm install\nadded 1 package";
</script>
<CopyButton {code} transform={stripPrompts} />parseAnsi / AnsiOutput
parseAnsi now handles more of what real CLI output throws at it. OSC sequences (npm/cargo hyperlinks, title sets) no longer leak as visible garbage; \r progress-bar frames overwrite instead of concatenating; and SGR 7/9/8 (reverse, strikethrough, conceal) are recognized. OSC 8 links surface as a link field that AnsiOutput renders as an anchor; reverse video swaps fg/bg before autoContrast runs.
<script>
import { AnsiOutput, CodeWindow } from "svelte-highlight";
const text =
"\x1b]8;;https://example.com\x07\x1b[32m✓\x1b[0m\x1b]8;;\x07 build succeeded";
</script>
<CodeWindow variant="terminal" title="bash">
<AnsiOutput {text} />
</CodeWindow>