logo

Marked Extensions

@humanspeak/svelte-markdown supports any marked extension that adds custom token types. Pass extensions via the extensions prop and provide renderers for the custom token types — either as component renderers or snippet overrides.

How It Works

Marked extensions define custom token types, each with a name property (e.g., inlineKatex, blockKatex, alert). When you pass extensions via the extensions prop, SvelteMarkdown:

  1. Registers the extension’s tokenizers internally so the lexer produces the custom tokens
  2. Extracts the token type names from the extensions array
  3. Makes those names available as both component renderer keys (renderers={{ inlineKatex: ... }}) and snippet override names ({#snippet inlineKatex(props)})

Each snippet or component receives the token’s own properties as props. For example, marked-katex-extension produces tokens with text and displayMode, so your renderer gets { text: string, displayMode: boolean }.

Finding Token Type Names

To discover the snippet/renderer names for any extension, check the name field in its extensions array:

// marked-katex-extension → tokens named "inlineKatex" and "blockKatex"
// → {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}
// → or renderers={{ inlineKatex: ..., blockKatex: ... }}

// A custom alert extension → token named "alert"
// → {#snippet alert(props)}
// → or renderers={{ alert: AlertComponent }}
// marked-katex-extension → tokens named "inlineKatex" and "blockKatex"
// → {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}
// → or renderers={{ inlineKatex: ..., blockKatex: ... }}

// A custom alert extension → token named "alert"
// → {#snippet alert(props)}
// → or renderers={{ alert: AlertComponent }}

You can also inspect the tokens at runtime using the parsed callback:

<SvelteMarkdown {source} {extensions} parsed={(tokens) => console.log(tokens)} />
<SvelteMarkdown {source} {extensions} parsed={(tokens) => console.log(tokens)} />

Step-by-Step: KaTeX Math Rendering

This example uses marked-katex-extension to add $...$ (inline) and $$...$$ (block) math syntax.

1. Install Dependencies

npm install marked-katex-extension katex
npm install marked-katex-extension katex

2. Create a Svelte Renderer

The marked-katex-extension produces tokens with text and displayMode properties. Create a Svelte component that calls katex.renderToString():

<!-- KatexRenderer.svelte -->
<script lang="ts">
    import katex from 'katex'

    interface Props {
        text: string
        displayMode?: boolean
    }

    const { text, displayMode = false }: Props = $props()

    const html = $derived(
        katex.renderToString(text, { throwOnError: false, displayMode })
    )
</script>

{@html html}
<!-- KatexRenderer.svelte -->
<script lang="ts">
    import katex from 'katex'

    interface Props {
        text: string
        displayMode?: boolean
    }

    const { text, displayMode = false }: Props = $props()

    const html = $derived(
        katex.renderToString(text, { throwOnError: false, displayMode })
    )
</script>

{@html html}

This single component handles both inlineKatex (displayMode = false) and blockKatex (displayMode = true) tokens.

3. Wire Up with Component Renderers

Pass the extension via the extensions prop and map token types to your component via renderers:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import markedKatex from 'marked-katex-extension'
    import KatexRenderer from './KatexRenderer.svelte'

    interface KatexRenderers extends Renderers {
        inlineKatex: RendererComponent
        blockKatex: RendererComponent
    }

    const renderers: Partial<KatexRenderers> = {
        inlineKatex: KatexRenderer,
        blockKatex: KatexRenderer
    }
</script>

<SvelteMarkdown
    source={markdown}
    extensions={[markedKatex({ throwOnError: false })]}
    {renderers}
/>
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import markedKatex from 'marked-katex-extension'
    import KatexRenderer from './KatexRenderer.svelte'

    interface KatexRenderers extends Renderers {
        inlineKatex: RendererComponent
        blockKatex: RendererComponent
    }

    const renderers: Partial<KatexRenderers> = {
        inlineKatex: KatexRenderer,
        blockKatex: KatexRenderer
    }
</script>

<SvelteMarkdown
    source={markdown}
    extensions={[markedKatex({ throwOnError: false })]}
    {renderers}
/>

4. Alternative: Snippet Overrides

Instead of component renderers, you can use snippet overrides for inline rendering:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import katex from 'katex'
    import markedKatex from 'marked-katex-extension'
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown
    source={markdown}
    extensions={[markedKatex({ throwOnError: false })]}
>
    {#snippet inlineKatex(props)}
        {@html katex.renderToString(props.text, { displayMode: false })}
    {/snippet}
    {#snippet blockKatex(props)}
        {@html katex.renderToString(props.text, { displayMode: true })}
    {/snippet}
</SvelteMarkdown>
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import katex from 'katex'
    import markedKatex from 'marked-katex-extension'
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown
    source={markdown}
    extensions={[markedKatex({ throwOnError: false })]}
>
    {#snippet inlineKatex(props)}
        {@html katex.renderToString(props.text, { displayMode: false })}
    {/snippet}
    {#snippet blockKatex(props)}
        {@html katex.renderToString(props.text, { displayMode: true })}
    {/snippet}
</SvelteMarkdown>

5. Include KaTeX CSS

KaTeX requires its stylesheet for proper math formatting:

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>
<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

Complete Example

Putting it all together with component renderers:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import markedKatex from 'marked-katex-extension'
    import KatexRenderer from './KatexRenderer.svelte'

    interface KatexRenderers extends Renderers {
        inlineKatex: RendererComponent
        blockKatex: RendererComponent
    }

    const renderers: Partial<KatexRenderers> = {
        inlineKatex: KatexRenderer,
        blockKatex: KatexRenderer
    }

    const source = `
# Euler's Identity

The equation $e^{i\\pi} + 1 = 0$ is considered the most beautiful in mathematics.

$$
e^{i\\pi} + 1 = 0
$$
`
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown
    {source}
    extensions={[markedKatex({ throwOnError: false })]}
    {renderers}
/>
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import markedKatex from 'marked-katex-extension'
    import KatexRenderer from './KatexRenderer.svelte'

    interface KatexRenderers extends Renderers {
        inlineKatex: RendererComponent
        blockKatex: RendererComponent
    }

    const renderers: Partial<KatexRenderers> = {
        inlineKatex: KatexRenderer,
        blockKatex: KatexRenderer
    }

    const source = `
# Euler's Identity

The equation $e^{i\\pi} + 1 = 0$ is considered the most beautiful in mathematics.

$$
e^{i\\pi} + 1 = 0
$$
`
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown
    {source}
    extensions={[markedKatex({ throwOnError: false })]}
    {renderers}
/>

Step-by-Step: Mermaid Diagrams

Unlike KaTeX (synchronous rendering via katex.renderToString()), Mermaid is async and browser-only — it requires the DOM and uses await mermaid.render(). The package provides built-in markedMermaid and MermaidRenderer helpers so you don’t need to write boilerplate.

1. Install Mermaid

Mermaid is an optional peer dependency — install it yourself:

npm install mermaid
npm install mermaid

2. Wire Up with Built-in Helpers

The package exports markedMermaid() (a zero-dep tokenizer for ```mermaid code blocks) and MermaidRenderer (a Svelte component that lazy-loads mermaid and renders SVG diagrams with dark mode support):

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'

    const markdown = `
\`\`\`mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action 1]
    B -->|No| D[Action 2]
\`\`\`
`

    interface MermaidRenderers extends Renderers {
        mermaid: RendererComponent
    }

    const renderers: Partial<MermaidRenderers> = {
        mermaid: MermaidRenderer
    }
</script>

<SvelteMarkdown
    source={markdown}
    extensions={[markedMermaid()]}
    {renderers}
/>
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'

    const markdown = `
\`\`\`mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action 1]
    B -->|No| D[Action 2]
\`\`\`
`

    interface MermaidRenderers extends Renderers {
        mermaid: RendererComponent
    }

    const renderers: Partial<MermaidRenderers> = {
        mermaid: MermaidRenderer
    }
</script>

<SvelteMarkdown
    source={markdown}
    extensions={[markedMermaid()]}
    {renderers}
/>

The built-in MermaidRenderer handles:

  • Dynamic import: import('mermaid') in onMount ensures it only loads in the browser
  • Async rendering: mermaid.render() returns a promise with the SVG string
  • Loading/error states: Handles the async lifecycle gracefully
  • Unique IDs: crypto.randomUUID() prevents collisions when multiple diagrams render
  • Theme reactivity: A MutationObserver watches the <html> class for dark/light changes and re-renders with the correct Mermaid theme via per-diagram directives

Snippet Overrides for Async Extensions

You can also use snippet overrides to wrap MermaidRenderer with custom markup — extra classes, wrapper divs, or surrounding content:

<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
    {#snippet mermaid(props)}
        <div class="my-diagram-wrapper">
            <MermaidRenderer text={props.text} />
        </div>
    {/snippet}
</SvelteMarkdown>
<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
    {#snippet mermaid(props)}
        <div class="my-diagram-wrapper">
            <MermaidRenderer text={props.text} />
        </div>
    {/snippet}
</SvelteMarkdown>

Since snippets run synchronously during render, they delegate the async work to MermaidRenderer rather than calling mermaid.render() directly. This pattern works for any async extension — keep the async logic in a component and use the snippet for layout customization.

General Pattern

The same pattern works for any marked extension:

  1. Install the extension package
  2. Discover the token type names — check the extension’s extensions[].name fields, or use the parsed callback to inspect tokens
  3. Pass the extension via the extensions prop
  4. Render the custom tokens — either map the token type name(s) to Svelte components via renderers, or use inline {#snippet tokenName(props)} overrides
  5. Include any required CSS or external resources

The token type name is the key that connects everything: it’s what the extension calls the token, what you use as the renderers key, and what you use as the snippet name.

Extension Ecosystem

The marked ecosystem includes extensions for:

  • Math: marked-katex-extension, marked-mathjax
  • Alerts/Admonitions: marked-alert, custom GFM-style alerts
  • Diagrams: Extensions wrapping Mermaid, PlantUML, etc.
  • Syntax highlighting: marked-highlight
  • Custom containers: marked-custom-heading-id, marked-footnote

Any extension that adds custom token types can be integrated using the pattern above.

Related