{"id":18234488,"url":"https://github.com/humanspeak/svelte-markdown","last_synced_at":"2026-05-05T01:03:48.296Z","repository":{"id":261108532,"uuid":"880536873","full_name":"humanspeak/svelte-markdown","owner":"humanspeak","description":"📝 Fast, lightweight Markdown renderer component for Svelte applications with full CommonMark support","archived":false,"fork":false,"pushed_at":"2025-10-08T17:17:07.000Z","size":4108,"stargazers_count":59,"open_issues_count":1,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-25T21:34:57.185Z","etag":null,"topics":["javascript","markdown","markdown-parser","markdown-to-html","svelte","sveltekit","typescript","ui-components","web-components"],"latest_commit_sha":null,"homepage":"https://markdown.svelte.page","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/humanspeak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["humanspeak"]}},"created_at":"2024-10-29T22:38:46.000Z","updated_at":"2025-10-20T19:00:40.000Z","dependencies_parsed_at":"2024-11-04T19:03:25.017Z","dependency_job_id":"22642d3b-f8c6-406a-936a-4a5d43f89de1","html_url":"https://github.com/humanspeak/svelte-markdown","commit_stats":{"total_commits":114,"total_committers":15,"mean_commits":7.6,"dds":0.6140350877192983,"last_synced_commit":"1818cc08684bb3a3ebd94de2077113a48039c378"},"previous_names":["humanspeak/svelte-markdown"],"tags_count":65,"template":false,"template_full_name":null,"purl":"pkg:github/humanspeak/svelte-markdown","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humanspeak%2Fsvelte-markdown","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humanspeak%2Fsvelte-markdown/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humanspeak%2Fsvelte-markdown/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humanspeak%2Fsvelte-markdown/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/humanspeak","download_url":"https://codeload.github.com/humanspeak/svelte-markdown/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/humanspeak%2Fsvelte-markdown/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29098115,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-04T21:05:08.033Z","status":"ssl_error","status_checked_at":"2026-02-04T21:04:53.031Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","markdown","markdown-parser","markdown-to-html","svelte","sveltekit","typescript","ui-components","web-components"],"created_at":"2024-11-04T22:03:24.972Z","updated_at":"2026-05-05T01:03:48.286Z","avatar_url":"https://github.com/humanspeak.png","language":"TypeScript","funding_links":["https://github.com/sponsors/humanspeak"],"categories":[],"sub_categories":[],"readme":"# @humanspeak/svelte-markdown\n\nA powerful, customizable markdown renderer for Svelte with TypeScript support. Built as a successor to the original svelte-markdown package by Pablo Berganza, now maintained and enhanced by Humanspeak, Inc.\n\n[![NPM version](https://img.shields.io/npm/v/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)\n[![Build Status](https://github.com/humanspeak/svelte-markdown/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/humanspeak/svelte-markdown/actions/workflows/npm-publish.yml)\n[![Coverage Status](https://coveralls.io/repos/github/humanspeak/svelte-markdown/badge.svg?branch=main)](https://coveralls.io/github/humanspeak/svelte-markdown?branch=main)\n[![License](https://img.shields.io/npm/l/@humanspeak/svelte-markdown.svg)](https://github.com/humanspeak/svelte-markdown/blob/main/LICENSE)\n[![Downloads](https://img.shields.io/npm/dm/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)\n[![CodeQL](https://github.com/humanspeak/svelte-markdown/actions/workflows/codeql.yml/badge.svg)](https://github.com/humanspeak/svelte-markdown/actions/workflows/codeql.yml)\n[![Install size](https://packagephobia.com/badge?p=@humanspeak/svelte-markdown)](https://packagephobia.com/result?p=@humanspeak/svelte-markdown)\n[![Code Style: Trunk](https://img.shields.io/badge/code%20style-trunk-blue.svg)](https://trunk.io)\n[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)\n[![Types](https://img.shields.io/npm/types/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)\n[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/humanspeak/svelte-markdown/graphs/commit-activity)\n\n## Features\n\n- 🔒 **Secure HTML parsing** via HTMLParser2 with XSS protection\n- 🚀 Full markdown syntax support through Marked\n- 💪 Complete TypeScript support with strict typing\n- 🔄 Svelte 5 runes compatibility\n- ✂️ Inline snippet overrides — customize renderers without separate files\n- 🎨 Customizable component rendering system\n- ♿ WCAG 2.1 accessibility compliance\n- 🎯 GitHub-style slug generation for headers\n- 🧪 Comprehensive test coverage (vitest and playwright)\n- 🧩 First-class marked extensions support via `extensions` prop (e.g., KaTeX math, alerts)\n- ⚡ Intelligent token caching (50-200x faster re-renders)\n- 📡 LLM streaming mode with incremental rendering (~1.6ms avg per update)\n- 🖼️ Smart image lazy loading with fade-in animation\n\n## Installation\n\n```bash\nnpm i -S @humanspeak/svelte-markdown\n```\n\nOr with your preferred package manager:\n\n```bash\npnpm add @humanspeak/svelte-markdown\nyarn add @humanspeak/svelte-markdown\n```\n\n## Basic Usage\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n\n    const source = `\n# This is a header\n\nThis is a paragraph with **bold** and \u003cem\u003emixed HTML\u003c/em\u003e.\n\n* List item with \\`inline code\\`\n* And a [link](https://svelte.dev)\n  * With nested items\n  * Supporting full markdown\n`\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} /\u003e\n```\n\n## TypeScript Support\n\nThe package is written in TypeScript and includes full type definitions:\n\n```typescript\nimport type {\n    Renderers,\n    Token,\n    TokensList,\n    SvelteMarkdownOptions,\n    MarkedExtension\n} from '@humanspeak/svelte-markdown'\n```\n\n## Exports for programmatic overrides\n\nYou can import renderer maps and helper keys to selectively override behavior.\n\n```ts\nimport SvelteMarkdown, {\n    // Maps\n    defaultRenderers, // markdown renderer map\n    Html, // HTML renderer map\n\n    // Keys\n    rendererKeys, // markdown renderer keys (excludes 'html')\n    htmlRendererKeys, // HTML renderer tag names\n\n    // Utility components\n    Unsupported, // markdown-level unsupported fallback\n    UnsupportedHTML // HTML-level unsupported fallback\n} from '@humanspeak/svelte-markdown'\n\n// Example: override a subset\nconst customRenderers = {\n    ...defaultRenderers,\n    link: CustomLink,\n    html: {\n        ...Html,\n        span: CustomSpan\n    }\n}\n\n// Optional: iterate keys when building overrides dynamically\nfor (const key of rendererKeys) {\n    // if (key === 'paragraph') customRenderers.paragraph = MyParagraph\n}\nfor (const tag of htmlRendererKeys) {\n    // if (tag === 'div') customRenderers.html.div = MyDiv\n}\n```\n\nNotes\n\n- `rendererKeys` intentionally excludes `html`. Use `htmlRendererKeys` for HTML tag overrides.\n- `Unsupported` and `UnsupportedHTML` are available if you want a pass-through fallback strategy.\n\n## Helper utilities for allow/deny strategies\n\nThese helpers make it easy to either allow only a subset or exclude only a subset of renderers without writing huge maps by hand.\n\n- **HTML helpers**\n    - `buildUnsupportedHTML()`: returns a map where every HTML tag uses `UnsupportedHTML`.\n    - `allowHtmlOnly(allowed)`: enable only the provided tags; others use `UnsupportedHTML`.\n        - Accepts tag names like `'strong'` or tuples like `['div', MyDiv]` to plug in custom components.\n    - `excludeHtmlOnly(excluded, overrides?)`: disable only the listed tags (mapped to `UnsupportedHTML`), with optional overrides for non-excluded tags using tuples.\n- **Markdown helpers (non-HTML)**\n    - `buildUnsupportedRenderers()`: returns a map where all markdown renderers (except `html`) use `Unsupported`.\n    - `allowRenderersOnly(allowed)`: enable only the provided markdown renderer keys; others use `Unsupported`.\n        - Accepts keys like `'paragraph'` or tuples like `['paragraph', MyParagraph]` to plug in custom components.\n    - `excludeRenderersOnly(excluded, overrides?)`: disable only the listed markdown renderer keys, with optional overrides for non-excluded keys using tuples.\n\n### HTML helpers in context\n\nThe HTML helpers return an `HtmlRenderers` map to be used inside the `html` key of the overall `renderers` map. They do not replace the entire `renderers` object by themselves.\n\nBasic: keep markdown defaults, allow only a few HTML tags (others become `UnsupportedHTML`):\n\n```ts\nimport SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'\n\nconst renderers = {\n    ...defaultRenderers, // keep markdown defaults\n    html: allowHtmlOnly(['strong', 'em', 'a']) // restrict HTML\n}\n```\n\nAllow a custom component for one tag while allowing others with defaults:\n\n```ts\nimport SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'\n\nconst renderers = {\n    ...defaultRenderers,\n    html: allowHtmlOnly([['div', MyDiv], 'a'])\n}\n```\n\nExclude just a few HTML tags; keep all other HTML tags as defaults:\n\n```ts\nimport SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'\n\nconst renderers = {\n    ...defaultRenderers,\n    html: excludeHtmlOnly(['span', 'iframe'])\n}\n\n// Or exclude 'span', but override 'a' to CustomA\nconst renderersWithOverride = {\n    ...defaultRenderers,\n    html: excludeHtmlOnly(['span'], [['a', CustomA]])\n}\n```\n\nDisable all HTML quickly (markdown defaults unchanged):\n\n```ts\nimport SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'\n\nconst renderers = {\n    ...defaultRenderers,\n    html: buildUnsupportedHTML()\n}\n```\n\n### Markdown-only (non-HTML) scenarios\n\nAllow only paragraph and link with defaults, disable others:\n\n```ts\nimport { allowRenderersOnly } from '@humanspeak/svelte-markdown'\n\nconst md = allowRenderersOnly(['paragraph', 'link'])\n```\n\nExclude just link; keep others as defaults:\n\n```ts\nimport { excludeRenderersOnly } from '@humanspeak/svelte-markdown'\n\nconst md = excludeRenderersOnly(['link'])\n```\n\nDisable all markdown renderers (except `html`) quickly:\n\n```ts\nimport { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'\n\nconst md = buildUnsupportedRenderers()\n```\n\n### Combine HTML and Markdown helpers\n\nYou can combine both maps in `renderers` for `SvelteMarkdown`.\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown, { allowRenderersOnly, allowHtmlOnly } from '@humanspeak/svelte-markdown'\n\n    const renderers = {\n        // Only allow a minimal markdown set\n        ...allowRenderersOnly(['paragraph', 'link']),\n\n        // Configure HTML separately (only strong/em/a)\n        html: allowHtmlOnly(['strong', 'em', 'a'])\n    }\n\n    const source = `# Title\\n\\nThis has \u003cstrong\u003eHTML\u003c/strong\u003e and [a link](https://example.com).`\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} {renderers} /\u003e\n```\n\n## Custom Renderer Example\n\nHere's a complete example of a custom renderer with TypeScript support:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import type { Snippet } from 'svelte'\n\n    interface Props {\n        children?: Snippet\n        href?: string\n        title?: string\n    }\n\n    const { href = '', title = '', children }: Props = $props()\n\u003c/script\u003e\n\n\u003ca {href} {title} class=\"custom-link\"\u003e\n    {@render children?.()}\n\u003c/a\u003e\n```\n\nIf you would like to extend other renderers please take a look inside the [renderers folder](https://github.com/humanspeak/svelte-markdown/tree/main/src/lib/renderers) for the default implentation of them. If you would like feature additions please feel free to open an issue!\n\n## Snippet Overrides (Svelte 5)\n\nFor simple tweaks — adding a class, changing an attribute, wrapping in a div — you can override renderers inline with Svelte 5 snippets instead of creating separate component files:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n\n    const source = '# Hello\\n\\nA paragraph with [a link](https://example.com).'\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source}\u003e\n    {#snippet paragraph({ children })}\n        \u003cp class=\"prose\"\u003e{@render children?.()}\u003c/p\u003e\n    {/snippet}\n\n    {#snippet heading({ depth, children })}\n        {#if depth === 1}\n            \u003ch1 class=\"title\"\u003e{@render children?.()}\u003c/h1\u003e\n        {:else}\n            \u003ch2\u003e{@render children?.()}\u003c/h2\u003e\n        {/if}\n    {/snippet}\n\n    {#snippet link({ href, title, children })}\n        \u003ca {href} {title} target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n            {@render children?.()}\n        \u003c/a\u003e\n    {/snippet}\n\n    {#snippet code({ lang, text })}\n        \u003cpre class=\"highlight {lang}\"\u003e\u003ccode\u003e{text}\u003c/code\u003e\u003c/pre\u003e\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\n### How it works\n\n- **Container renderers** (paragraph, heading, blockquote, list, etc.) receive a `children` snippet for nested content\n- **Leaf renderers** (code, image, hr, br) receive only data props — no `children`\n- **Precedence**: snippet \u003e component renderer \u003e default. If both a snippet and a `renderers.paragraph` component are provided, the snippet wins\n\n### HTML tag snippets\n\nHTML tag snippets use an `html_` prefix to avoid collisions with markdown renderer names:\n\n```svelte\n\u003cSvelteMarkdown {source}\u003e\n    {#snippet html_div({ attributes, children })}\n        \u003cdiv class=\"custom-wrapper\" {...attributes}\u003e{@render children?.()}\u003c/div\u003e\n    {/snippet}\n\n    {#snippet html_a({ attributes, children })}\n        \u003ca {...attributes} target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n            {@render children?.()}\n        \u003c/a\u003e\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\nAll HTML snippets share a uniform props interface: `{ attributes?: Record\u003cstring, any\u003e, children?: Snippet }`.\n\n### Custom HTML Tags\n\nYou can render arbitrary (non-standard) HTML tags like `\u003cclick\u003e`, `\u003ctooltip\u003e`, or any custom element by providing a renderer or snippet for the tag name. The parsing pipeline accepts any tag name — you just need to tell `SvelteMarkdown` how to render it.\n\n**Component renderer approach:**\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import ClickButton from './ClickButton.svelte'\n\n    const source = '\u003cclick\u003eClick Me\u003c/click\u003e'\n    const renderers = { html: { click: ClickButton } }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} {renderers} /\u003e\n```\n\n**Snippet override approach:**\n\n```svelte\n\u003cSvelteMarkdown source={'\u003cclick data-action=\"submit\"\u003eClick Me\u003c/click\u003e'}\u003e\n    {#snippet html_click({ attributes, children })}\n        \u003cbutton {...attributes} class=\"custom-btn\"\u003e{@render children?.()}\u003c/button\u003e\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\nBoth approaches work for any tag name. Snippet overrides take precedence over component renderers when both are provided.\n\n## Marked Extensions\n\nUse [marked extensions](https://marked.js.org/using_advanced#extensions) via the `extensions` prop. SvelteMarkdown ships first-class extensions for KaTeX, Mermaid, GitHub-style alerts, and footnotes from the `@humanspeak/svelte-markdown/extensions` subpath — no third-party packages required. Third-party extensions still work too; the component handles registering tokenizers internally and you just provide renderers for the custom token types.\n\n### KaTeX Math Rendering\n\nThe package includes built-in `markedKatex` and `KatexRenderer` helpers. Install `katex` as an optional peer dependency and load its CSS:\n\n```bash\nnpm install katex\n```\n\n**Default delimiter set** (mirrors KaTeX's own [`auto-render`](https://katex.org/docs/autorender.html) defaults):\n\n| Delimiter pair                                                 | Level  | `displayMode` |\n| -------------------------------------------------------------- | ------ | ------------- |\n| `\\(...\\)`                                                      | inline | `false`       |\n| `\\[...\\]` (own-line)                                           | block  | `true`        |\n| `$$...$$` (own-line)                                           | block  | `true`        |\n| `\\begin{equation}...\\end{equation}` and other AMS environments | block  | `true`        |\n\nSingle-dollar inline (`$x^2$`) is **off** by default — KaTeX itself excludes it from auto-render to avoid currency-string clashes like `$5,000`. Pass `{ singleDollarInline: true }` to enable it; it uses a whitespace-bounded rule so currency strings still won't match.\n\n**Component renderer approach:**\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'\n    import { markedKatex, KatexRenderer } from '@humanspeak/svelte-markdown/extensions'\n\n    interface KatexRenderers extends Renderers {\n        inlineKatex: RendererComponent\n        blockKatex: RendererComponent\n    }\n\n    const renderers: Partial\u003cKatexRenderers\u003e = {\n        inlineKatex: KatexRenderer,\n        blockKatex: KatexRenderer\n    }\n\u003c/script\u003e\n\n\u003csvelte:head\u003e\n    \u003clink\n        rel=\"stylesheet\"\n        href=\"https://cdn.jsdelivr.net/npm/katex@0.16.45/dist/katex.min.css\"\n        crossorigin=\"anonymous\"\n    /\u003e\n\u003c/svelte:head\u003e\n\n\u003cSvelteMarkdown\n    source={`Euler's identity: \\\\(e^{i\\\\pi} + 1 = 0\\\\)`}\n    extensions={[markedKatex()]}\n    {renderers}\n/\u003e\n```\n\n`KatexRenderer` hardcodes `throwOnError: false` so a single malformed expression renders as a tinted error span instead of throwing — if you need stricter behavior, supply your own component for the `inlineKatex` / `blockKatex` keys.\n\n**Snippet override approach** (no separate component file needed):\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import { markedKatex } from '@humanspeak/svelte-markdown/extensions'\n    import katex from 'katex'\n\u003c/script\u003e\n\n\u003csvelte:head\u003e\n    \u003clink\n        rel=\"stylesheet\"\n        href=\"https://cdn.jsdelivr.net/npm/katex@0.16.45/dist/katex.min.css\"\n        crossorigin=\"anonymous\"\n    /\u003e\n\u003c/svelte:head\u003e\n\n\u003cSvelteMarkdown source={`Euler's identity: \\\\(e^{i\\\\pi} + 1 = 0\\\\)`} extensions={[markedKatex()]}\u003e\n    {#snippet inlineKatex(props)}\n        {@html katex.renderToString(props.text, { throwOnError: false, displayMode: false })}\n    {/snippet}\n    {#snippet blockKatex(props)}\n        {@html katex.renderToString(props.text, { throwOnError: false, displayMode: true })}\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\n### Mermaid Diagrams (Async Rendering)\n\nThe package includes built-in `markedMermaid` and `MermaidRenderer` helpers for Mermaid diagram support. Install mermaid as an optional peer dependency:\n\n```bash\nnpm install mermaid\n```\n\nThen use the built-in helpers — no boilerplate needed:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'\n    import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'\n\n    // markdown containing fenced mermaid code blocks\n    let { source } = $props()\n\n    interface MermaidRenderers extends Renderers {\n        mermaid: RendererComponent\n    }\n\n    const renderers: Partial\u003cMermaidRenderers\u003e = {\n        mermaid: MermaidRenderer\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} extensions={[markedMermaid()]} {renderers} /\u003e\n```\n\n`markedMermaid()` is a zero-dependency tokenizer that converts ` ```mermaid ` code blocks into custom tokens. `MermaidRenderer` lazy-loads mermaid in the browser, renders SVG asynchronously, and automatically re-renders when dark/light mode changes.\n\nYou can also use snippet overrides to wrap `MermaidRenderer` with custom markup:\n\n```svelte\n\u003cSvelteMarkdown source={markdown} extensions={[markedMermaid()]}\u003e\n    {#snippet mermaid(props)}\n        \u003cdiv class=\"my-diagram-wrapper\"\u003e\n            \u003cMermaidRenderer text={props.text} /\u003e\n        \u003c/div\u003e\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\nSince Mermaid rendering is async, the snippet delegates 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.\n\n### GitHub Alerts\n\nBuilt-in support for [GitHub-style alerts/admonitions](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts). Five alert types are supported: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'\n    import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'\n\n    const source = `\n\u003e [!NOTE]\n\u003e Useful information that users should know.\n\n\u003e [!WARNING]\n\u003e Urgent info that needs immediate attention.\n`\n\n    interface AlertRenderers extends Renderers {\n        alert: RendererComponent\n    }\n\n    const renderers: Partial\u003cAlertRenderers\u003e = {\n        alert: AlertRenderer\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} extensions={[markedAlert()]} {renderers} /\u003e\n```\n\n`AlertRenderer` renders a `\u003cdiv class=\"markdown-alert markdown-alert-{type}\"\u003e` with a title — no inline styles, so you can theme it with your own CSS. You can also use snippet overrides:\n\n```svelte\n\u003cSvelteMarkdown source={markdown} extensions={[markedAlert()]}\u003e\n    {#snippet alert(props)}\n        \u003cdiv class=\"my-alert my-alert-{props.alertType}\"\u003e\n            \u003cstrong\u003e{props.alertType}\u003c/strong\u003e\n            \u003cp\u003e{props.text}\u003c/p\u003e\n        \u003c/div\u003e\n    {/snippet}\n\u003c/SvelteMarkdown\u003e\n```\n\n### Footnotes\n\nBuilt-in support for footnote references and definitions. Footnote references (`[^id]`) render as superscript links, and definitions (`[^id]: content`) render as a numbered list at the end of the document with back-links.\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'\n    import {\n        markedFootnote,\n        FootnoteRef,\n        FootnoteSection\n    } from '@humanspeak/svelte-markdown/extensions'\n\n    const source = `\nHere is a statement[^1] with a footnote.\n\nAnother claim[^note] that needs a source.\n\n[^1]: This is the first footnote.\n[^note]: This is a named footnote.\n`\n\n    interface FootnoteRenderers extends Renderers {\n        footnoteRef: RendererComponent\n        footnoteSection: RendererComponent\n    }\n\n    const renderers: Partial\u003cFootnoteRenderers\u003e = {\n        footnoteRef: FootnoteRef,\n        footnoteSection: FootnoteSection\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} extensions={[markedFootnote()]} {renderers} /\u003e\n```\n\n`FootnoteRef` renders `\u003csup\u003e\u003ca href=\"#fn-{id}\"\u003e{id}\u003c/a\u003e\u003c/sup\u003e` and `FootnoteSection` renders an `\u003col\u003e` with bidirectional links (ref to definition and back). You can also use snippet overrides for custom rendering.\n\n### How It Works\n\nMarked extensions define custom token types with a `name` property (e.g., `inlineKatex`, `blockKatex`, `alert`). When you pass extensions via the `extensions` prop, SvelteMarkdown automatically extracts these token type names and makes them available as both **component renderer keys** and **snippet override names**.\n\nTo find the token type names for any extension, check its source or documentation for the `name` field in its `extensions` array:\n\n```js\n// Example: markedKatex (built-in) registers tokens named \"inlineKatex\" and \"blockKatex\"\n// → use renderers={{ inlineKatex: ..., blockKatex: ... }}\n// → or {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}\n\n// Example: a custom alert extension registers a token named \"alert\"\n// → use renderers={{ alert: AlertComponent }}\n// → or {#snippet alert(props)}\n```\n\nEach snippet/component receives the token's properties as props (e.g., `text`, `displayMode` for KaTeX; `text`, `level` for alerts).\n\nSee the [full documentation](https://markdown.svelte.page/docs/advanced/marked-extensions) and [interactive demo](https://markdown.svelte.page/examples/marked-extensions).\n\n### TypeScript\n\nAll snippet prop types are exported for use in external components:\n\n```typescript\nimport type {\n    ParagraphSnippetProps,\n    HeadingSnippetProps,\n    LinkSnippetProps,\n    CodeSnippetProps,\n    HtmlSnippetProps,\n    SnippetOverrides,\n    HtmlSnippetOverrides\n} from '@humanspeak/svelte-markdown'\n```\n\n## Advanced Features\n\n### Table Support with Mixed Content\n\nThe package excels at handling complex nested structures and mixed content:\n\n```markdown\n| Type       | Content                                 |\n| ---------- | --------------------------------------- |\n| Nested     | \u003cdiv\u003e**bold** and _italic_\u003c/div\u003e        |\n| Mixed List | \u003cul\u003e\u003cli\u003eItem 1\u003c/li\u003e\u003cli\u003eItem 2\u003c/li\u003e\u003c/ul\u003e |\n| Code       | \u003ccode\u003e`inline code`\u003c/code\u003e              |\n```\n\n### HTML in Markdown\n\nSeamlessly mix HTML and Markdown:\n\n```markdown\n\u003cdiv style=\"color: blue\"\u003e\n  ### This is a Markdown heading inside HTML\n  And here's some **bold** text too!\n\u003c/div\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to expand\u003c/summary\u003e\n\n- This is a markdown list\n- Inside an HTML details element\n- Supporting **bold** and _italic_ text\n\n\u003c/details\u003e\n```\n\n## Performance\n\n### Intelligent Token Caching\n\nParsed tokens are automatically cached using an LRU strategy, providing 50-200x faster re-renders for previously seen content (\u003c 1ms vs 50-200ms). The cache uses FNV-1a hashing keyed on source + options, with LRU eviction (default 50 documents) and TTL expiration (default 5 minutes). No configuration required.\n\n```typescript\nimport { tokenCache, TokenCache } from '@humanspeak/svelte-markdown'\n\n// Manual cache management\ntokenCache.clearAllTokens()\ntokenCache.deleteTokens(markdown, options)\n\n// Custom cache instance\nconst myCache = new TokenCache({ maxSize: 100, ttl: 10 * 60 * 1000 })\n```\n\n### Smart Image Lazy Loading\n\nImages automatically lazy load using native `loading=\"lazy\"` and IntersectionObserver prefetching, with a smooth fade-in animation and error state handling. To disable lazy loading, provide a custom Image renderer:\n\n```svelte\n\u003c!-- EagerImage.svelte --\u003e\n\u003cscript lang=\"ts\"\u003e\n    let { href = '', title = undefined, text = '' } = $props()\n\u003c/script\u003e\n\n\u003cimg src={href} {title} alt={text} loading=\"eager\" /\u003e\n```\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import EagerImage from './EagerImage.svelte'\n\n    const renderers = { image: EagerImage }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown source={markdown} {renderers} /\u003e\n```\n\n### LLM Streaming\n\nFor real-time rendering of AI responses from ChatGPT, Claude, Gemini, and other LLMs, enable the `streaming` prop. This uses a smart diff algorithm that re-parses the full source for correctness but only updates changed DOM nodes, keeping render times constant regardless of document size.\n\nThe preferred API is now imperative: bind the component instance and call `writeChunk()` as chunks arrive. This avoids prop reactivity edge cases like identical consecutive string chunks being coalesced.\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n    import type { StreamingChunk } from '@humanspeak/svelte-markdown'\n\n    let markdown:\n        | {\n              writeChunk: (chunk: StreamingChunk) =\u003e void\n              resetStream: (nextSource?: string) =\u003e void\n          }\n        | undefined\n\n    async function streamResponse() {\n        const response = await fetch('/api/chat', { method: 'POST', body: '...' })\n        const reader = response.body.getReader()\n        const decoder = new TextDecoder()\n\n        while (true) {\n            const { done, value } = await reader.read()\n            if (done) break\n            markdown?.writeChunk(decoder.decode(value, { stream: true }))\n        }\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown bind:this={markdown} source=\"\" streaming={true} /\u003e\n```\n\nFor websocket-style offset patches, pass an object chunk instead:\n\n```ts\nmarkdown?.writeChunk({ value: 'world', offset: 6 })\n```\n\nObject chunks overwrite the internal buffer at `offset`. This is overwrite semantics, not insert semantics: the chunk replaces characters starting at that index and preserves any trailing content after the overwritten span.\n\nIf `offset` skips ahead, missing positions are padded with spaces. There is no delete or truncate behavior in offset mode.\n\nTypical websocket-style usage can arrive out of order:\n\n```ts\nmarkdown?.writeChunk({ value: ' world', offset: 5 })\nmarkdown?.writeChunk({ value: 'Hello', offset: 0 })\n```\n\nThe internal buffer converges as later patches fill earlier gaps.\n\nYou can reset the internal streaming buffer at any time:\n\n```ts\nmarkdown?.resetStream('')\nmarkdown?.resetStream('# Seeded response')\n```\n\nThe first successful write after a reset locks the stream into one input mode:\n\n- `string` chunks: append mode\n- `{ value, offset }` chunks: offset mode\n\nSwitching modes before `resetStream()` or a `source` prop reset logs a warning and drops the chunk. Offset chunks must use a non-negative safe integer `offset`.\n\nChanging the `source` prop also resets the imperative buffer, seeds a new baseline value, and unlocks the input mode.\n\nAppending directly to `source` is still supported:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n\n    let source = $state('')\n\n    function onChunk(chunk: string) {\n        source += chunk\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} streaming={true} /\u003e\n```\n\n**Performance** (measured at 100 characters/sec, character mode):\n\n| Metric         | Standard Mode | Streaming Mode |\n| -------------- | :-----------: | :------------: |\n| Average render |    ~3.6ms     |     ~1.6ms     |\n| Peak render    |     ~21ms     |     ~10ms      |\n| Dropped frames |       0       |       0        |\n\nWhen `streaming` is `false` (default), existing behavior is unchanged. The `streaming` prop skips cache lookups (always a miss during streaming) and uses in-place token array mutation so Svelte only re-renders components for tokens that actually changed.\n\n**Note:** `streaming` is automatically disabled when async extensions (e.g., `markedMermaid`) are used. A console warning is logged in this case.\n\nSee the [full streaming documentation](https://markdown.svelte.page/docs/advanced/llm-streaming) and [interactive demo](https://markdown.svelte.page/examples/llm-streaming).\n\n## Available Renderers\n\n- `text` - Text within other elements\n- `paragraph` - Paragraph (`\u003cp\u003e`)\n- `em` - Emphasis (`\u003cem\u003e`)\n- `strong` - Strong/bold (`\u003cstrong\u003e`)\n- `hr` - Horizontal rule (`\u003chr\u003e`)\n- `blockquote` - Block quote (`\u003cblockquote\u003e`)\n- `del` - Deleted/strike-through (`\u003cdel\u003e`)\n- `link` - Link (`\u003ca\u003e`)\n- `image` - Image (`\u003cimg\u003e`)\n- `table` - Table (`\u003ctable\u003e`)\n- `tablehead` - Table head (`\u003cthead\u003e`)\n- `tablebody` - Table body (`\u003ctbody\u003e`)\n- `tablerow` - Table row (`\u003ctr\u003e`)\n- `tablecell` - Table cell (`\u003ctd\u003e`/`\u003cth\u003e`)\n- `list` - List (`\u003cul\u003e`/`\u003col\u003e`)\n- `listitem` - List item (`\u003cli\u003e`)\n- `heading` - Heading (`\u003ch1\u003e`-`\u003ch6\u003e`)\n- `codespan` - Inline code (`\u003ccode\u003e`)\n- `code` - Block of code (`\u003cpre\u003e\u003ccode\u003e`)\n- `html` - HTML node\n- `rawtext` - All other text that is going to be included in an object above\n\n### Optional List Renderers\n\nFor fine-grained styling:\n\n- `orderedlistitem` - Items in ordered lists\n- `unorderedlistitem` - Items in unordered lists\n\n### HTML Renderers\n\nThe `html` renderer is special and can be configured separately to handle HTML elements:\n\n| Element  | Description          |\n| -------- | -------------------- |\n| `div`    | Division element     |\n| `span`   | Inline container     |\n| `table`  | HTML table structure |\n| `thead`  | Table header group   |\n| `tbody`  | Table body group     |\n| `tr`     | Table row            |\n| `td`     | Table data cell      |\n| `th`     | Table header cell    |\n| `ul`     | Unordered list       |\n| `ol`     | Ordered list         |\n| `li`     | List item            |\n| `code`   | Code block           |\n| `em`     | Emphasized text      |\n| `strong` | Strong text          |\n| `a`      | Anchor/link          |\n| `img`    | Image                |\n\nYou can customize HTML rendering by providing your own components:\n\n```typescript\nimport type { HtmlRenderers } from '@humanspeak/svelte-markdown'\n\nconst customHtmlRenderers: Partial\u003cHtmlRenderers\u003e = {\n    div: YourCustomDivComponent,\n    span: YourCustomSpanComponent\n}\n```\n\n## Events\n\nThe component emits a `parsed` event when tokens are calculated:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n    import SvelteMarkdown from '@humanspeak/svelte-markdown'\n\n    const handleParsed = (tokens: Token[] | TokensList) =\u003e {\n        console.log('Parsed tokens:', tokens)\n    }\n\u003c/script\u003e\n\n\u003cSvelteMarkdown {source} parsed={handleParsed} /\u003e\n```\n\n## Props\n\n| Prop       | Type                    | Description                                      |\n| ---------- | ----------------------- | ------------------------------------------------ |\n| source     | `string \\| Token[]`     | Markdown content or pre-parsed tokens            |\n| streaming  | `boolean`               | Enable incremental rendering for LLM streaming   |\n| renderers  | `Partial\u003cRenderers\u003e`    | Custom component overrides                       |\n| options    | `SvelteMarkdownOptions` | Marked parser configuration                      |\n| isInline   | `boolean`               | Toggle inline parsing mode                       |\n| extensions | `MarkedExtension[]`     | Third-party marked extensions (e.g., KaTeX math) |\n\n## Security\n\nThis package takes a defense-in-depth approach to security:\n\n- **Secure HTML parsing** - All HTML is parsed through HTMLParser2's streaming parser rather than `innerHTML`, preventing script injection\n- **XSS protection** - HTML entities are safely handled; malicious markdown injection is neutralized during parsing\n- **Granular HTML control** - Use `allowHtmlOnly()` / `excludeHtmlOnly()` to restrict which HTML tags are rendered (see [Helper utilities](#helper-utilities-for-allowdeny-strategies))\n- **Full HTML lockdown** - Call `buildUnsupportedHTML()` to block all raw HTML rendering\n- **Markdown renderer control** - Use `allowRenderersOnly()` / `excludeRenderersOnly()` to limit which markdown token types are rendered\n- **No built-in sanitizer** - By design, the package does not bundle a sanitizer. Integrate your own (e.g., DOMPurify) if you accept untrusted input\n\n## License\n\nMIT © [Humanspeak, Inc.](LICENSE)\n\n## Credits\n\nMade with ❤️ by [Humanspeak](https://humanspeak.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumanspeak%2Fsvelte-markdown","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhumanspeak%2Fsvelte-markdown","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumanspeak%2Fsvelte-markdown/lists"}