<!-- Source: https://markdown.svelte.page/examples/shiki -->

# Syntax Highlighting (Shiki)

> Highlight code blocks in @humanspeak/svelte-markdown with the opt-in Shiki extension — a synchronous, streaming-compatible ShikiCode renderer with a safe escaped fallback.

**Source:** [https://markdown.svelte.page/examples/shiki](https://markdown.svelte.page/examples/shiki)

**Markdown mirror:** [https://markdown.svelte.page/examples/shiki.md](https://markdown.svelte.page/examples/shiki.md)

---

This mirror preserves the prose, implementation notes, and runnable Svelte source behind the live example page.

## FIG-001: shiki renderer.

Map `ShikiCode` to the `code` renderer and inject a highlighter via Svelte context. Highlighting runs synchronously, so streaming stays enabled — edit the markdown live.

**Metadata:** tag: `COMPONENT` | override: `component`

### Notes

- `ShikiCode` is a drop-in for the built-in `code` renderer (same `lang` / `text` props) — only fenced blocks change.
- Shiki's `createHighlighterCoreSync` + the JS regex engine highlight synchronously, so the async-extension guard never trips and `streaming` stays on.
- Import only the `shiki/langs/*` and `shiki/themes/*` you need — everything else is tree-shaken out, and the core bundle stays shiki-free.

### Source

#### ComponentRendered.svelte

Source file: [src/lib/examples/shiki/demos/ComponentRendered.svelte](https://github.com/humanspeak/svelte-markdown/blob/main/docs/src/lib/examples/shiki/demos/ComponentRendered.svelte)

````svelte
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import {
        createShikiHighlighter,
        ShikiCode,
        SHIKI_CONTEXT_KEY
    } from '@humanspeak/svelte-markdown/extensions/shiki'
    import { DemoSplitV2 } from '@humanspeak/docs-kit'
    import { setContext } from 'svelte'
    import js from 'shiki/langs/javascript.mjs'
    import json from 'shiki/langs/json.mjs'
    import ts from 'shiki/langs/typescript.mjs'
    import githubDark from 'shiki/themes/github-dark.mjs'

    // Per-subtree injection via Svelte context — isolated from any other
    // highlighter on the page (no global module-singleton cross-talk).
    // ShikiCode reads this context at init and highlights synchronously.
    setContext(
        SHIKI_CONTEXT_KEY,
        createShikiHighlighter({ langs: [js, ts, json], themes: [githubDark] })
    )

    // ShikiCode is a drop-in for the built-in `code` renderer (same lang/text
    // props), so only fenced blocks are affected — inline `code` is untouched.
    interface CodeRenderers extends Renderers {
        code: RendererComponent
    }
    const renderers: Partial<CodeRenderers> = {
        code: ShikiCode
    }

    const defaultMarkdown = `## Shiki Highlighting

Fenced blocks render through Shiki synchronously — no async work, so
**streaming stays enabled**.

\`\`\`ts
interface User {
    id: number
    name: string
}

const greet = (user: User): string => \`Hello, \${user.name}!\`
\`\`\`

\`\`\`json
{ "langs": ["javascript", "typescript", "json"], "theme": "github-dark" }
\`\`\`

Inline \`code\` uses the codespan renderer and is left untouched.`

    // Debounced live editor: `input` tracks the textarea verbatim, `source`
    // is what SvelteMarkdown renders.
    let input = $state(defaultMarkdown)
    let source = $state(defaultMarkdown)
    let debounceTimer = $state<ReturnType<typeof setTimeout> | undefined>(undefined)

    function handleInput(event: Event) {
        const target = event.target as HTMLTextAreaElement
        input = target.value
        if (debounceTimer) clearTimeout(debounceTimer)
        debounceTimer = setTimeout(() => {
            source = input
        }, 300)
    }
</script>

<DemoSplitV2 leftLabel="EDITOR" leftTone="markdown" rightLabel="PREVIEW" rightTone="diagrams">
    {#snippet left()}
        <textarea
            value={input}
            oninput={handleInput}
            class="md-editor"
            spellcheck="false"
            placeholder="Type markdown with ```ts / ```json code blocks..."></textarea>
    {/snippet}
    {#snippet right()}
        <div class="md-preview prose prose-sm dark:prose-invert max-w-none">
            <SvelteMarkdown {source} {renderers} />
        </div>
    {/snippet}
</DemoSplitV2>

<style>
    .md-editor {
        width: 100%;
        height: 100%;
        min-height: 360px;
        resize: none;
        border: 0;
        outline: none;
        background: transparent;
        color: var(--brut-ink, var(--foreground, inherit));
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
        font-size: 12.5px;
        line-height: 1.7;
        padding: 0;
    }
    .md-preview {
        font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
        color: var(--brut-ink-2, var(--muted-foreground, inherit));
    }
    .md-preview :global(h1),
    .md-preview :global(h2),
    .md-preview :global(h3),
    .md-preview :global(h4) {
        color: var(--brut-ink, var(--foreground, inherit));
        letter-spacing: -0.02em;
    }
    /* Shiki emits <pre class="shiki"> with its own theme background. */
    .md-preview :global(pre.shiki),
    .md-preview :global(pre.shiki-fallback) {
        padding: 12px;
        border-radius: 6px;
        overflow-x: auto;
        font-size: 12px;
        line-height: 1.6;
    }
    .md-preview :global(pre.shiki-fallback) {
        background: var(--brut-bg-2, rgba(127, 127, 127, 0.08));
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.18));
    }
    .md-preview :global(:not(pre) > code) {
        background: var(--brut-bg-2, rgba(127, 127, 127, 0.08));
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.18));
        padding: 0 4px;
        font-size: 12px;
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
    }
</style>
````

## FIG-002: safe fallback.

Unregistered languages (and any per-block failure) degrade to an escaped `<pre class="shiki-fallback">` instead of throwing mid-stream — the untrusted lang is only ever an escaped `data-lang`.

**Metadata:** tag: `FALLBACK` | lang: `unregistered`

### Notes

- The fenced `lang` is untrusted (agent/LLM input) — the fallback escapes, never interpolates it, emitting it only as an escaped `data-lang`.
- Shiki escapes the code it emits and the fallback escapes too, so the `{@html}` sink only ever receives library-generated or escaped markup.
- Style `.shiki-fallback` to match your registered blocks — the content is a plain escaped ` `.

### Source

#### FallbackRendered.svelte

Source file: [src/lib/examples/shiki/demos/FallbackRendered.svelte](https://github.com/humanspeak/svelte-markdown/blob/main/docs/src/lib/examples/shiki/demos/FallbackRendered.svelte)

```svelte
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import {
        createShikiHighlighter,
        ShikiCode,
        SHIKI_CONTEXT_KEY
    } from '@humanspeak/svelte-markdown/extensions/shiki'
    import { DemoSplitV2 } from '@humanspeak/docs-kit'
    import { setContext } from 'svelte'
    import ts from 'shiki/langs/typescript.mjs'
    import githubDark from 'shiki/themes/github-dark.mjs'

    // Only `typescript` is registered here — everything else degrades safely.
    setContext(SHIKI_CONTEXT_KEY, createShikiHighlighter({ langs: [ts], themes: [githubDark] }))

    interface CodeRenderers extends Renderers {
        code: RendererComponent
    }
    const renderers: Partial<CodeRenderers> = {
        code: ShikiCode
    }

    // `rust` is not registered, so it falls back to an escaped
    // <pre class="shiki-fallback"> instead of throwing mid-stream. The
    // untrusted lang string is only ever emitted as an escaped data-lang.
    const source = `### Registered language (\`ts\`)

\`\`\`ts
const total: number = [1, 2, 3].reduce((a, b) => a + b, 0)
\`\`\`

### Unregistered language (\`rust\`) — safe escaped fallback

\`\`\`rust
fn main() {
    let markup = "<b>bold</b> & <i>italic</i>";
    println!("{markup}");
}
\`\`\``
</script>

<DemoSplitV2 leftLabel="SOURCE" leftTone="markdown" rightLabel="PREVIEW" rightTone="diagrams">
    {#snippet left()}
        <pre class="md-source">{source}</pre>
    {/snippet}
    {#snippet right()}
        <div class="md-preview prose prose-sm dark:prose-invert max-w-none">
            <SvelteMarkdown {source} {renderers} />
        </div>
    {/snippet}
</DemoSplitV2>

<style>
    .md-source {
        margin: 0;
        white-space: pre-wrap;
        color: var(--brut-ink, var(--foreground, inherit));
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
        font-size: 12.5px;
        line-height: 1.7;
    }
    .md-preview {
        font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
        color: var(--brut-ink-2, var(--muted-foreground, inherit));
    }
    .md-preview :global(h1),
    .md-preview :global(h2),
    .md-preview :global(h3),
    .md-preview :global(h4) {
        color: var(--brut-ink, var(--foreground, inherit));
        letter-spacing: -0.02em;
    }
    .md-preview :global(pre.shiki),
    .md-preview :global(pre.shiki-fallback) {
        padding: 12px;
        border-radius: 6px;
        overflow-x: auto;
        font-size: 12px;
        line-height: 1.6;
    }
    .md-preview :global(pre.shiki-fallback) {
        background: var(--brut-bg-2, rgba(127, 127, 127, 0.08));
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.18));
        color: var(--brut-ink, var(--foreground, inherit));
    }
</style>
```
