Skip to content

v7.15.0

Choose a tag to compare

@metonym metonym released this 11 Jul 04:16
· 542 commits to master since this release
v7.15.0
9fe263d

Features

  • add HighlightStream for incrementally streamed code (51754f6)
  • add experimental CSS Custom Highlight engine for HighlightEditable (6ed5775)
  • add transform hook to CopyButton (d27dcd4, #447)
  • handle OSC, carriage returns, and more SGR in parseAnsi (09dc980, #449)

Fixes

  • make the highlight action idempotent and reversible (4475d08, #455)
  • coerce non-string code before highlighting (3319f17, #456)
  • recompute HighlightStyle scope class on theme change (719a297, #454)
  • re-highlight HighlightEditable when 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 Typewriter units without per-tick retokenization (0db8217, #450)
  • repaint only changed lines in HighlightEditable (3bff41e)
  • track selection only while HighlightEditable is focused (51615d6)
  • store HighlightEditable history as slice diffs (c094a18)
  • read HighlightEditable's code via textContent, not innerText (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>