{"id":30788608,"url":"https://github.com/saluana/streamdown-vue","last_synced_at":"2026-04-12T02:36:32.707Z","repository":{"id":312920084,"uuid":"1049221017","full_name":"Saluana/streamdown-vue","owner":"Saluana","description":" Streamdown style streaming Markdown to Vue 3 \u0026 Nuxt 3","archived":false,"fork":false,"pushed_at":"2025-09-03T01:53:10.000Z","size":289,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-03T03:34:10.399Z","etag":null,"topics":["ai","llm","markdown","markdown-stream","streamdown","vercel"],"latest_commit_sha":null,"homepage":"","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/Saluana.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"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}},"created_at":"2025-09-02T16:54:07.000Z","updated_at":"2025-09-03T01:53:14.000Z","dependencies_parsed_at":"2025-09-03T03:34:11.240Z","dependency_job_id":null,"html_url":"https://github.com/Saluana/streamdown-vue","commit_stats":null,"previous_names":["saluana/vuedown","saluana/streamdown-vue"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/Saluana/streamdown-vue","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Saluana%2Fstreamdown-vue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Saluana%2Fstreamdown-vue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Saluana%2Fstreamdown-vue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Saluana%2Fstreamdown-vue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Saluana","download_url":"https://codeload.github.com/Saluana/streamdown-vue/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Saluana%2Fstreamdown-vue/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273761884,"owners_count":25163363,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-05T02:00:09.113Z","response_time":402,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["ai","llm","markdown","markdown-stream","streamdown","vercel"],"created_at":"2025-09-05T13:18:12.244Z","updated_at":"2026-04-12T02:36:32.692Z","avatar_url":"https://github.com/Saluana.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# streamdown-vue\n\n`streamdown-vue` brings [Streamdown](https://github.com/vercel/streamdown)-style streaming Markdown to Vue 3 \u0026 Nuxt 3. It ships a `\u003cStreamMarkdown\u003e` component that incrementally renders Markdown as it arrives (token‑by‑token, chunk‑by‑chunk), plus helper utilities to keep partially received text valid.\n\n---\n\n## Table of Contents\n\n1. Features\n2. Installation\n3. Quick Start (Basic SSR + CSR)\n4. Default Styling \u0026 Customization\n5. Deep Dive Tutorial (Streaming from an AI / SSE source)\n6. Props Reference (All `\u003cStreamMarkdown\u003e` props)\n7. Component Slots \u0026 Overrides\n8. Built‑in Components \u0026 Data Attributes\n9. Security Model (Link/Image hardening)\n10. Syntax Highlighting (Shiki), Copy / Download \u0026 Extensible Actions\n11. Mermaid Diagrams\n12. Math \u0026 LaTeX Fixes\n13. Utilities (`parseBlocks`, `parseIncompleteMarkdown`, LaTeX helpers)\n14. Performance Tips\n15. Nuxt 3 Usage \u0026 SSR Notes\n16. Recipe Gallery\n17. FAQ\n18. Development \u0026 Contributing\n\n---\n\n## 1. Features\n\n-   GitHub‑flavored Markdown (tables, task lists, strikethrough) via `remark-gfm`\n-   KaTeX math (`remark-math` + `rehype-katex`) with extra repairs (matrices, stray `$`)\n-   Shiki syntax highlighting (light + dark themes) with reactive copy \u0026 download buttons\n    and an extensible action bar (add your own buttons globally or per-instance)\n-   Mermaid diagrams with caching, async render \u0026 graceful error recovery\n-   Incremental rendering + repair of incomplete Markdown tokens while streaming\n-   Secure allow‑list based hardening of link \u0026 image URLs (blocks `javascript:` etc.)\n-   Component override layer (swap any tag / embed custom Vue components)\n-   Data attributes for each semantic element (`data-streamdown=\"...\"`) for styling/testing\n-   Designed for SSR (Vue / Nuxt) \u0026 fast hydration; tree‑shakable, side‑effects minimized\n\n---\n\n## 2. Installation\n\n### Bun\n\n```bash\nbun add streamdown-vue\n```\n\n### npm / pnpm / yarn\n\n```bash\nnpm install streamdown-vue\n# pnpm add streamdown-vue\n# yarn add streamdown-vue\n```\n\nYou must also install peer deps `vue` (and optionally `typescript`).\n\nInclude KaTeX stylesheet once (if you use math):\n\n```ts\nimport 'katex/dist/katex.min.css';\n```\n\n---\n\n## 3. Quick Start\n\n`main.ts`:\n\n```ts\nimport { createApp } from 'vue';\nimport App from './App.vue';\nimport 'katex/dist/katex.min.css';\ncreateApp(App).mount('#app');\n```\n\n`App.vue`:\n\n```vue\n\u003ctemplate\u003e\n    \u003cStreamMarkdown class=\"prose\" :content=\"markdown\" /\u003e\n\u003c/template\u003e\n\u003cscript setup lang=\"ts\"\u003e\nimport { StreamMarkdown } from 'streamdown-vue';\nconst markdown = `# Hello\\n\\nSome *markdown* with $e^{i\\\\pi}+1=0$.`;\n\u003c/script\u003e\n```\n\nSSR (server) minimal snippet:\n\n```ts\nimport { renderToString } from '@vue/server-renderer';\nimport { createSSRApp, h } from 'vue';\nimport { StreamMarkdown } from 'streamdown-vue';\n\nconst app = createSSRApp({\n    render: () =\u003e h(StreamMarkdown, { content: '# SSR' }),\n});\nconst html = await renderToString(app);\n```\n\n---\n\n## 4. Default Styling \u0026 Customization\n\n### Importing the Default Stylesheet\n\n`streamdown-vue` ships with an optional built-in stylesheet that provides clean, neutral, and compact styling for all markdown elements. To use it:\n\n```ts\nimport 'streamdown-vue/style.css';\n```\n\n**What's included:**\n- Neutral color scheme with automatic dark mode support\n- Compact, professional styling for tables and code blocks\n- Subtle, transparent scrollbars\n- Properly styled buttons, headings, lists, and blockquotes\n- Line number styling (when enabled)\n- All styles are scoped to `.streamdown-vue` to avoid conflicts\n\n**Example:**\n\n```ts\n// main.ts\nimport { createApp } from 'vue';\nimport App from './App.vue';\nimport 'streamdown-vue/style.css'; // ← Import default styles\nimport 'katex/dist/katex.min.css';\n\ncreateApp(App).mount('#app');\n```\n\n### Customizing with CSS Variables\n\nAll styles use CSS variables prefixed with `--sd-*` that you can override. See the [full list of variables in style.css](./src/style.css).\n\n```css\n:root {\n  /* Colors */\n  --sd-primary: #3b82f6;              /* Accent color for links, headings */\n  --sd-primary-variant: #2563eb;      /* Hover states */\n  --sd-on-surface: #1f2937;           /* Text color */\n  --sd-surface-container: #f3f4f6;    /* Code block backgrounds */\n  --sd-border-color: #e5e7eb;         /* Borders */\n  \n  /* Typography */\n  --sd-font-family-base: system-ui, sans-serif;\n  --sd-font-family-mono: ui-monospace, monospace;\n  --sd-font-size-base: 16px;\n  --sd-line-height-base: 1.7;\n  \n  /* Dimensions */\n  --sd-border-width: 1px;\n  --sd-border-radius: 0.375rem;\n}\n```\n\n**Example - Custom Brand Colors:**\n\n```css\n:root {\n  --sd-primary: #8b5cf6;        /* Purple accent */\n  --sd-primary-variant: #7c3aed;\n}\n```\n\n### Dark Mode\n\nDark mode is automatic via `@media (prefers-color-scheme: dark)` or by adding the `.dark` class to your `\u003chtml\u003e` element:\n\n```ts\n// Toggle dark mode\nconst toggleDark = () =\u003e {\n  document.documentElement.classList.toggle('dark');\n};\n```\n\nYou can customize dark mode colors:\n\n```css\n:root.dark {\n  --sd-primary: #60a5fa;              /* Lighter blue for dark mode */\n  --sd-on-surface: #f3f4f6;           /* Light text */\n  --sd-surface-container: #374151;    /* Dark gray backgrounds */\n  --sd-border-color: #374151;\n}\n```\n\n### Completely Custom Styling\n\nIf you prefer to write your own styles from scratch, simply **don't import** `streamdown-vue/style.css`. All markdown elements have `data-streamdown` attributes for easy targeting:\n\n```css\n/* Your custom styles */\n.streamdown-vue [data-streamdown='code-block'] {\n  /* Custom code block styling */\n}\n\n.streamdown-vue [data-streamdown='table'] {\n  /* Custom table styling */\n}\n```\n\nSee section 9 (Built-in Components \u0026 Data Attributes) for a complete list of available attributes.\n\n---\n\n## 5. Deep Dive Tutorial – Live Streaming (AI / SSE)\n\nWhen receiving tokens / partial chunks you typically want to:\n\n1. Append new text chunk into a buffer.\n2. Repair the partial Markdown (`parseIncompleteMarkdown`).\n3. Split into safe blocks for re-render (`parseBlocks`).\n4. Feed the concatenated repaired text to `\u003cStreamMarkdown\u003e`.\n\nComposable example (client side):\n\n```ts\n// useStreamedMarkdown.ts\nimport { ref } from 'vue';\nimport { parseBlocks, parseIncompleteMarkdown } from 'streamdown-vue';\n\nexport function useStreamedMarkdown() {\n    const rawBuffer = ref('');\n    const rendered = ref('');\n    const blocks = ref\u003cstring[]\u003e([]);\n\n    const pushChunk = (text: string) =\u003e {\n        rawBuffer.value += text;\n        // repair incomplete tokens (unclosed **, `, $$, etc.)\n        const repaired = parseIncompleteMarkdown(rawBuffer.value);\n        blocks.value = parseBlocks(repaired);\n        rendered.value = blocks.value.join('');\n    };\n\n    return { rawBuffer, rendered, blocks, pushChunk };\n}\n```\n\nUsing Server-Sent Events (SSE):\n\n```ts\nconst { rendered, pushChunk } = useStreamedMarkdown();\nconst es = new EventSource('/api/chat');\nes.onmessage = (e) =\u003e {\n    pushChunk(e.data);\n};\nes.onerror = () =\u003e es.close();\n```\n\nTemplate:\n\n```vue\n\u003cStreamMarkdown :content=\"rendered\" /\u003e\n```\n\nWhy repair first? Without repair, a trailing `**` or lone ``` will invalidate the final tree and cause flicker or lost highlighting. Repairing keeps intermediate renders stable.\n\n---\n\n## 6. Props Reference\n\n| Prop                       | Type                       | Default                  | Description                                                                                                                                                                     |\n| -------------------------- | -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `content`                  | `string`                   | `''`                     | The full (or partially streamed) markdown source.                                                                                                                               |\n| `class` / `className`      | `string`                   | `''`                     | Optional wrapper classes; both accepted (React-style alias).                                                                                                                    |\n| `components`               | `Record\u003cstring,Component\u003e` | `{}`                     | Map to override built-ins (e.g. `{ p: MyP }`).                                                                                                                                  |\n| `remarkPlugins`            | `any[]`                    | `[]`                     | Extra remark plugins. Supports `(plugin)` or `[plugin, options]`. If you supply `remark-math` yourself, the built‑in one (which disables single‑dollar inline math) is skipped. |\n| `rehypePlugins`            | `any[]`                    | `[]`                     | Extra rehype plugins.                                                                                                                                                           |\n| `defaultOrigin`            | `string?`                  | `undefined`              | Base URL used to resolve relative links/images before allow‑list checks.                                                                                                        |\n| `allowedImagePrefixes`     | `string[]`                 | `['https://','http://']` | Allowed (lowercased) URL prefixes for `\u003cimg\u003e`. Blocked =\u003e image dropped.                                                                                                        |\n| `allowedLinkPrefixes`      | `string[]`                 | `['https://','http://']` | Allowed prefixes for `\u003ca href\u003e`. Blocked =\u003e link text only.                                                                                                                     |\n| `parseIncompleteMarkdown`  | `boolean`                  | `true`                   | (Future toggle) Auto apply repair internally. Currently you repair outside using utility; prop reserved.                                                                        |\n| `shikiTheme`               | `ShikiThemeConfig`         | `undefined`              | Shiki theme (string) or dual theme object `{ light: '...', dark: '...' }`. If undefined, follows system preference (`github-light`/`github-dark`).                            |\n| `codeBlockActions`         | `Component[]`              | `[]`                     | Array of Vue components appended as action buttons in every code block header.                                                                                                  |\n| `codeBlockShowLineNumbers` | `boolean`                  | `false`                  | Show line numbers in all code fences.                                                                                                                                           |\n| `codeBlockSelectable`      | `boolean`                  | `true`                   | Whether code text is selectable (adds `select-none` when false).                                                                                                                |\n| `codeBlockHideCopy`        | `boolean`                  | `false`                  | Globally hide built‑in copy buttons (you can add your own via actions).                                                                                                         |\n| `codeBlockHideDownload`    | `boolean`                  | `false`                  | Globally hide built‑in download buttons.                                                                                                                                        |\n\nAll unrecognised props are ignored (no arbitrary HTML injection for safety).\n\n---\n\n## 7. Component Slots \u0026 Overrides\n\n`\u003cStreamMarkdown\u003e` does not expose custom slots for content fragments (the pipeline is AST-driven). To customize rendering you override tags via the `components` prop:\n\n```ts\nimport type { Component } from 'vue';\nimport { StreamMarkdown } from 'streamdown-vue';\n\nconst FancyP: Component = {\n    setup(_, { slots }) { return () =\u003e h('p', { class: 'text-pink-600 font-serif' }, slots.default?.()); }\n};\n\n\u003cStreamMarkdown :components=\"{ p: FancyP }\" :content=\"md\" /\u003e\n```\n\nIf a tag is missing from `components` it falls back to the built-in map.\n\n---\n\n## 8. Built‑in Components \u0026 Data Attributes\n\nEach semantic node receives a `data-streamdown=\"name\"` attribute to make styling and querying reliable, even if classes are overridden:\n\n| Element / Component        | Data Attribute      | Notes / Styling Hook                                                   |\n| -------------------------- | ------------------- | ---------------------------------------------------------------------- |\n| Paragraph `\u003cp\u003e`            | `p`                 | Base text blocks                                                       |\n| Anchor `\u003ca\u003e`               | `a`                 | Hardened links (target+rel enforced)                                   |\n| Inline code `\u003ccode\u003e`       | `inline-code`       | Single backtick spans                                                  |\n| Code block wrapper         | `code-block`        | Outer container (header + body)                                        |\n| Code block header bar      | `code-block-header` | Holds language label + copy button                                     |\n| Code language badge        | `code-lang`         | Language label span                                                    |\n| Empty language placeholder | `code-lang-empty`   | Present when no language specified (reserved space)                    |\n| Copy button                | `copy-button`       | The actionable copy control                                            |\n| Code block body container  | `code-body`         | Wraps highlighted `\u003cpre\u003e`; horizontal scroll applied here              |\n| Code block \u003cpre\u003e element   | `pre`               | Added automatically to inner `\u003cpre\u003e` for targeting styles              |\n| Code block \u003ccode\u003e element  | `code`              | Added automatically to inner `\u003ccode\u003e`                                  |\n| Code line number span      | `code-line-number`  | Present when line numbers enabled                                      |\n| Unordered list `\u003cul\u003e`      | `ul`                |                                                                        |\n| Ordered list `\u003col\u003e`        | `ol`                |                                                                        |\n| List item `\u003cli\u003e`           | `li`                |                                                                        |\n| Horizontal rule `\u003chr\u003e`     | `hr`                |                                                                        |\n| Strong `\u003cstrong\u003e`          | `strong`            | Bold emphasis                                                          |\n| Emphasis `\u003cem\u003e`            | `em`                | Italic emphasis                                                        |\n| Headings `\u003ch1\u003e`–`\u003ch6\u003e`     | `h1` … `h6`         | Each level individually tagged                                         |\n| Blockquote `\u003cblockquote\u003e`  | `blockquote`        |                                                                        |\n| Table `\u003ctable\u003e`            | `table`             | Logical table element                                                  |\n| Table wrapper `\u003cdiv\u003e`      | `table-wrapper`     | Scroll container around table                                          |\n| Table head `\u003cthead\u003e`       | `thead`             |                                                                        |\n| Table body `\u003ctbody\u003e`       | `tbody`             |                                                                        |\n| Table row `\u003ctr\u003e`           | `tr`                |                                                                        |\n| Table header cell `\u003cth\u003e`   | `th`                |                                                                        |\n| Table data cell `\u003ctd\u003e`     | `td`                |                                                                        |\n| Image `\u003cimg\u003e`              | `img`               | Only if src passes hardening                                           |\n| Mermaid wrapper            | `mermaid`           | Replaced with rendered SVG / diagram                                   |\n| KaTeX output               | `katex`             | Class emitted by KaTeX (not set by us but styled via global KaTeX CSS) |\n\n### 8.1 Styling via Data Attributes\n\nBecause every semantic node has a stable `data-streamdown` marker, you can author zero‑collision styles (or component library themes) without relying on brittle tag chains. Example – customize the code block body and header:\n\n```css\n/* Remove borders \u0026 add extra bottom padding inside code body */\n.message-body :deep([data-streamdown='code-body']) pre {\n    border: none;\n    margin-bottom: 0;\n    padding-bottom: 30px;\n}\n\n/* Header bar tweaks */\n.message-body :deep([data-streamdown='code-block-header']) {\n    background: linear-gradient(to right, #f5f5f5, #e8e8e8);\n    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Language badge */\n.message-body :deep([data-streamdown='code-lang']) {\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n}\n\n/* Table wrapper scroll shadows */\n.message-body :deep([data-streamdown='table-wrapper']) {\n    position: relative;\n}\n.message-body :deep([data-streamdown='table-wrapper']::after) {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    width: 12px;\n    pointer-events: none;\n    background: linear-gradient(\n        to right,\n        rgba(255, 255, 255, 0),\n        rgba(0, 0, 0, 0.08)\n    );\n}\n```\n\nTips:\n\n1. Scope via a parent (e.g. `.message-body`) or component root to avoid leaking styles.\n2. Use `:deep()` (Vue SFC) / `::v-deep` where needed to pierce scoped boundaries.\n3. Prefer attribute selectors over tag names so overrides survive internal structural changes.\n4. For dark mode, pair selectors with media queries or a `.dark` ancestor.\n\nTesting example (Vitest / Bun):\n\n```ts\nexpect(html).toContain('data-streamdown=\"inline-code\"');\n```\n\n---\n\n## 9. Security Model\n\nOnly absolute URLs starting with an allowed prefix pass. Steps:\n\n1. Resolve relative (`/x`) against `defaultOrigin` if provided.\n2. Lowercase \u0026 check `javascript:` scheme (blocked).\n3. Check each allowed prefix (case-insensitive).\n4. If any fail, the element is dropped (link/text downgraded, image removed).\n\nExample – allow only your CDN images \u0026 HTTPS links:\n\n```vue\n\u003cStreamMarkdown\n    :allowed-link-prefixes=\"['https://']\"\n    :allowed-image-prefixes=\"['https://cdn.example.com/']\"\n    default-origin=\"https://example.com\"\n    :content=\"md\"\n/\u003e\n```\n\n---\n\n## 10. Syntax Highlighting (Shiki), Copy / Download \u0026 Extensible Actions\n\nCode fences are rendered by the internal `CodeBlock` component:\n\n````md\n```ts\nconst x: number = 1;\n```\n````\n\nOverride with your custom block:\n\n```ts\nimport { defineComponent, h } from 'vue';\nimport { useShikiHighlighter } from 'streamdown-vue';\n\nconst MyCode = defineComponent({\n    props: { code: { type: String, required: true }, language: { type: String, default: '' } },\n    async setup(props) {\n        const highlighter = await useShikiHighlighter();\n        const html = highlighter.codeToHtml(props.code, { lang: props.language || 'text', themes: { light: 'github-light', dark: 'github-dark' } });\n        return () =\u003e h('div', { class: 'my-code', innerHTML: html });\n    }\n});\n\n\u003cStreamMarkdown :components=\"{ codeblock: MyCode }\" /\u003e\n```\n\n\u003e **Need a deeper walkthrough?** The [Shiki Language Bundling Guide](./docs/shiki-language-guide.md) covers both the batteries-included entry and the minimal core workflow.\n\n### 10.1 Changing the Shiki Theme\n\nYou can switch the built‑in highlighting theme via the `shikiTheme` prop.\n\n**Default Behavior:** If you do not provide a theme, it defaults to `undefined`. This triggers automatic system preference detection:\n- `github-light` when the user's system is in light mode\n- `github-dark` when the user's system is in dark mode (via `prefers-color-scheme: dark`)\n\nTo force a specific theme:\n\n```vue\n\u003cStreamMarkdown :content=\"md\" shiki-theme=\"github-dark\" /\u003e\n```\n\nAny valid Shiki theme name you have available can be passed.\n\n### 10.2 Dual Theme Support (Automatic Theme Switching)\n\nInstead of manually watching theme changes and reactively updating the prop, you can provide both light and dark themes for instant CSS-based theme switching:\n\n```vue\n\u003c!-- Dual theme (recommended) - instant switching via CSS --\u003e\n\u003cStreamMarkdown \n  :shiki-theme=\"{ light: 'github-light', dark: 'github-dark' }\" \n  :content=\"md\" \n/\u003e\n```\n\nWhen using dual themes:\n- Shiki generates CSS variables for both themes in a single render\n- Theme switching happens instantly via CSS class changes (no component re-render needed)\n- Add/remove `.dark` class on a parent element or rely on `@media (prefers-color-scheme: dark)`\n- No need to watch theme changes reactively\n- More efficient - eliminates unnecessary re-renders when toggling themes\n\n**Comparison:**\n\n```vue\n\u003c!-- Single theme (old approach) - requires reactive updates \u0026 re-renders --\u003e\n\u003cStreamMarkdown \n  :shiki-theme=\"isDark ? 'github-dark' : 'github-light'\" \n  :content=\"md\" \n/\u003e\n\n\u003c!-- Dual theme (new, recommended) - automatic via CSS --\u003e\n\u003cStreamMarkdown \n  :shiki-theme=\"{ light: 'github-light', dark: 'github-dark' }\" \n  :content=\"md\" \n/\u003e\n```\n\nThe dual theme approach is the recommended pattern for applications with theme switching.\n\n\u003e Note: The default build registers a compact set of common languages (TS/JS/JSON/Bash/Python/Diff/Markdown/Vue/HTML/CSS/Go/Rust/YAML). Add or remove grammars by calling `registerShikiLanguage(s)` before your first render (details below).\n\n### 10.3 Controlling the Shiki bundle\n\n- **`registerDefaultShikiLanguages()`** – invoked automatically when you import from `streamdown-vue`; registers the curated language set.\n- **`registerShikiLanguage(s)`** – call this in your own entry point to add or override grammars (local files or CDN-based loaders).\n- **`excludeShikiLanguages([...])`** – remove specific loaders after the defaults register (e.g. drop Rust if you never show it).\n- **`clearRegisteredShikiLanguages()`** – wipe the registry entirely before registering your own minimal set (used internally by the core entry).\n- **`streamdown-vue/core` entry** – ships without defaults so only the grammars you register are ever referenced (perfect for bundle-sensitive apps).\n\nRegistering only the languages you need (core build):\n\n```ts\nimport {\n    StreamMarkdown,\n    registerShikiLanguages,\n} from 'streamdown-vue/core';\n\nregisterShikiLanguages([\n    { id: 'typescript', loader: () =\u003e import('@shikijs/langs/typescript') },\n    { id: 'json', loader: () =\u003e import('@shikijs/langs/json') },\n    { id: 'bash', loader: () =\u003e import('@shikijs/langs/bash') },\n]);\n```\n\nThe default (non-core) entry automatically registers the curated set listed below. If you only ever highlight a smaller subset, switch to the core entry, register those languages, and your bundler will never even see the unused grammars.\n\n### 10.4 Preloaded Shiki Languages\n\nThe built-in highlighter eagerly loads the grammars you register. The default bundle calls `registerDefaultShikiLanguages()` which wires up the following set:\n\n| Canonical ID | Aliases        | Human-readable language |\n| ------------ | -------------- | ----------------------- |\n| `typescript` | `ts`           | TypeScript              |\n| `tsx`        | —              | TypeScript JSX          |\n| `javascript` | `js`           | JavaScript              |\n| `jsx`        | —              | JavaScript JSX          |\n| `json`       | —              | JSON                    |\n| `bash`       | —              | Bash / shell script     |\n| `shell`      | `shellscript`, `sh`, `zsh` | Generic shell script    |\n| `python`     | `py`           | Python                  |\n| `diff`       | —              | Unified diff            |\n| `markdown`   | `md`           | Markdown                |\n| `vue`        | `markdown-vue` | Vue SFC                 |\n| `html`       | `html-derivative` | HTML / derivatives  |\n| `css`        | —              | CSS                     |\n| `go`         | —              | Go                      |\n| `rust`       | —              | Rust                    |\n| `yaml`       | `yml`          | YAML                    |\n| `cpp`        | `c++`          | C++ *(CDN)* |\n| `java`       | —              | Java *(CDN)* |\n| `c`          | —              | C *(CDN)* |\n| `csharp`     | `cs`, `c#`     | C# *(CDN)* |\n| `php`        | —              | PHP *(CDN)* |\n| `ruby`       | —              | Ruby *(CDN)* |\n| `kotlin`     | —              | Kotlin *(CDN)* |\n| `swift`      | —              | Swift *(CDN)* |\n| `sql`        | —              | SQL *(CDN)* |\n\nIf you rarely show, for instance, Rust or Go snippets, simply omit them when calling `registerShikiLanguages`. Fences that reference an unregistered language fall back to plain `\u003cpre\u003e\u003ccode\u003e` (with a development warning) instead of pulling in extra grammars.\n\n### 10.5 Built‑in CodeBlock Features\n\n`CodeBlock` now provides:\n\n| Feature                    | Prop / Mechanism              | Default | Notes                                                                                  |\n| -------------------------- | ----------------------------- | ------- | -------------------------------------------------------------------------------------- |\n| Copy button                | `hideCopy` (boolean)          | `false` | Uses Clipboard API; auto‑binds code via context.                                       |\n| Download button            | `hideDownload` (boolean)      | `false` | Generates file with inferred extension (lightweight mapping).                          |\n| Line numbers               | `showLineNumbers` (boolean)   | `false` | Injects `\u003cspan class=\"code-line-number\" data-streamdown=\"code-line-number\"\u003e` prefixes. |\n| Selectability toggle       | `selectable` (boolean)        | `true`  | Adds `select-none` on `\u003cpre\u003e` when disabled.                                           |\n| Per‑block custom actions   | `:actions=\"[MyBtn]\"`          | `[]`    | Array of components/render fns appended right of header.                               |\n| Slot actions               | `\u003ctemplate #actions\u003e`         | —       | Slot for ad‑hoc buttons (highest flexibility).                                         |\n| Global actions             | App `provide`                 | —       | Provide once: `app.provide(GLOBAL_CODE_BLOCK_ACTIONS, [MyBtn])`.                       |\n| Context access for actions | `inject(CODE_BLOCK_META_KEY)` | —       | Retrieve `{ code, language }` without prop drilling.                                   |\n\n### 10.6 Adding Custom Action Buttons (Without Forking)\n\nYou normally only use `\u003cStreamMarkdown\u003e`; customize all code blocks via pass‑through props:\n\n```vue\n\u003cStreamMarkdown\n    :content=\"md\"\n    :code-block-actions=\"[MyShareButton]\"\n    code-block-show-line-numbers\n    code-block-hide-download\n/\u003e\n```\n\nOr override the internal code block entirely through `components` map (key: `codeblock`):\n\n```ts\nconst Minimal = defineComponent({\n    props: { code: String, language: String },\n    setup(p) { return () =\u003e h('pre', [h('code', p.code)]) }\n});\n\n\u003cStreamMarkdown :components=\"{ codeblock: Minimal }\" :content=\"md\" /\u003e\n```\n\nPer instance:\n\n```vue\n\u003cCodeBlock\n    :code=\"snippet\"\n    language=\"ts\"\n    :actions=\"[MyShareButton, MyRunButton]\"\n/\u003e\n```\n\nOr via named slot:\n\n```vue\n\u003cCodeBlock :code=\"snippet\" language=\"ts\"\u003e\n    \u003ctemplate #actions\u003e\n        \u003cMyShareButton /\u003e\n        \u003cMyRunButton /\u003e\n    \u003c/template\u003e\n\u003c/CodeBlock\u003e\n```\n\nGlobally (main.ts):\n\n```ts\nimport { GLOBAL_CODE_BLOCK_ACTIONS } from 'streamdown-vue';\napp.provide(GLOBAL_CODE_BLOCK_ACTIONS, [MyShareButton]);\n```\n\nInside a custom button component you can access the current code \u0026 language without props:\n\n```ts\nimport { defineComponent, inject } from 'vue';\nimport { CODE_BLOCK_META_KEY } from 'streamdown-vue';\n\nexport const MyShareButton = defineComponent({\n    setup() {\n        const meta = inject(CODE_BLOCK_META_KEY)!; // { code, language }\n        const share = () =\u003e navigator.share?.({ text: meta.code });\n        return () =\u003e \u003cbutton onClick={share}\u003eShare\u003c/button\u003e;\n    },\n});\n```\n\n### 10.7 Hiding Built‑ins\n\nIf you want a fully custom action bar:\n\n```vue\n\u003cCodeBlock\n    :code=\"snippet\"\n    language=\"ts\"\n    hide-copy\n    hide-download\n    :actions=\"[MyShareButton]\"\n/\u003e\n```\n\n### 10.8 Styling Line Numbers\n\nLine numbers render as `\u003cspan class=\"code-line-number\" data-line-number data-streamdown=\"code-line-number\"\u003e`. Example Tailwind tweaks:\n\n```css\n[data-streamdown='code-body'] .code-line-number {\n    @apply text-gray-400 dark:text-gray-500 select-none;\n}\n```\n\nThe default copy \u0026 download buttons can be selectively hidden while still using custom actions.\n\nThe default copy button uses the Clipboard API and toggles an icon for UX; the download button creates a Blob and triggers a synthetic click.\n\n---\n\n## 11. Mermaid Diagrams\n\nFenced block:\n\n````md\n```mermaid\ngraph TD;A--\u003eB;B--\u003eC;\n```\n````\n\nThe `MermaidBlock` component handles:\n\n-   Deduplicated initialization\n-   Simple hash based caching\n-   Error fallback (last good diagram)\n-   Copy diagram source\n\nYou can override it via `components` if you need advanced theming.\n\n---\n\n## 12. Math \u0026 LaTeX Helpers\n\n### 12.1 Default behavior\n\n`StreamMarkdown` automatically injects `remark-math` + `rehype-katex` _unless you supply your own_ via the `remarkPlugins` prop. The built‑in configuration intentionally sets `singleDollarTextMath: false` so that plain currency like `$390K` or `$80–140K` is **not** misinterpreted as inline math (a common issue during streaming where a later `$` closes a huge unintended span).\n\nSupported by default:\n\n-   Display math: `$$ ... $$`\n-   (If you add them) Inline math via `\\( ... \\)` or by providing your own `remark-math` with single‑dollar enabled.\n\n### 12.2 Opting into single‑dollar inline math\n\nIf you really want `$x + y$` style inline math, provide your own configured plugin tuple. When you do this the built‑in math plugin is skipped:\n\n```ts\nimport remarkMath from 'remark-math';\n\n\u003cStreamMarkdown\n    :content=\"md\"\n    :remark-plugins=\"[[remarkMath, { singleDollarTextMath: true }]]\"\n/\u003e\n```\n\n### 12.3 Optional helper utilities\n\nWe still expose some light repair helpers you can (optionally) run yourself before streaming completes:\n\n| Helper              | Purpose (opt‑in)                                                       |\n| ------------------- | ---------------------------------------------------------------------- |\n| `fixDollarSignMath` | (Optional) Escape truly stray `$` you decide are currency, if desired. |\n| `fixMatrix`         | Ensure matrix environments have proper row `\\\\` line breaks.           |\n\nExample (opt‑in):\n\n```ts\nimport { fixMatrix, fixDollarSignMath } from 'streamdown-vue';\n\nconst safe = fixMatrix(fixDollarSignMath(markdown));\n```\n\nIn streaming scenarios prefer leaving dollar signs untouched; the default config already avoids accidental inline math.\n\n---\n\n## 13. Utilities\n\n### `parseIncompleteMarkdown(text: string)`\n\nRepairs incomplete constructs (unclosed `**`, `_`, `` ` ``, `~~`, `$$` blocks, links/images) so partial buffers still render.\n\n### `parseBlocks(text: string)`\n\nTokenizes markdown into stable block strings; combining repaired buffer pieces reduces re‑parsing cost vs re‑feeding the whole document each keystroke.\n\nUsage inside a stream loop (see Tutorial above). Both exported from package root.\n\n---\n\n## 14. Performance Tips\n\n-   Debounce UI updates: apply repairs \u0026 re-render at ~30–60fps (e.g. `requestAnimationFrame`).\n-   Reuse a single `\u003cStreamMarkdown\u003e` instance; change only `content` prop.\n-   Avoid running large custom remark/rehype plugins on every partial—they run on full text.\n-   If highlighting is heavy for enormous fences, lazy-replace code block component after final chunk.\n-   Use server-side rendering for initial payload to reduce Total Blocking Time.\n\nBenchmarks (see `docs/performance.md`) show ~56ms render of the complex fixture under Bun (subject to change).\n\n---\n\n## 15. Nuxt 3 Usage \u0026 SSR Notes\n\nThis section shows end‑to‑end integration in a Nuxt 3 project: installation, global registration, a streaming composable, and a server route that emits incremental Markdown.\n\n### 15.1 Install\n\n```bash\nnpm i streamdown-vue\n# or: bun add streamdown-vue\n```\n\n### 15.2 Add a Client Plugin (Shiki + KaTeX)\n\nCreate `plugins/streamdown.client.ts` (client only so Shiki \u0026 Mermaid load in browser):\n\n```ts\n// plugins/streamdown.client.ts\nimport 'katex/dist/katex.min.css'; // once globally\n// (Optional) warm the Shiki highlighter so first code block is instant\nimport { useShikiHighlighter } from 'streamdown-vue';\nuseShikiHighlighter();\n```\n\nNuxt auto‑registers anything in `plugins/`. No manual config required unless you disabled auto import.\n\n### 15.3 Basic Page Usage\n\n```vue\n\u003c!-- pages/index.vue --\u003e\n\u003ctemplate\u003e\n    \u003cdiv class=\"prose mx-auto p-6\"\u003e\n        \u003cStreamMarkdown :content=\"md\" /\u003e\n    \u003c/div\u003e\n    \u003cfooter class=\"text-xs opacity-60 mt-8\"\u003e\n        Rendered with streamdown-vue\n    \u003c/footer\u003e\n\u003c/template\u003e\n\u003cscript setup lang=\"ts\"\u003e\nimport { StreamMarkdown } from 'streamdown-vue';\nconst md =\n    '# Welcome to Nuxt\\\\n\\\\nThis **Markdown** is rendered *streamdown style*.';\n\u003c/script\u003e\n```\n\n### 15.4 Global Component (Optional)\n\nIf you prefer auto‑import without explicit import each time, add an alias export file:\n\n```ts\n// components/StreamMarkdown.client.ts\nexport { StreamMarkdown as default } from 'streamdown-vue';\n```\n\nNow `\u003cStreamMarkdown /\u003e` is available automatically (Nuxt scans `components/`).\n\n### 15.5 Secure Link / Image Allow‑Lists\n\nIn any page/component:\n\n```vue\n\u003cStreamMarkdown\n    :content=\"md\"\n    :allowed-link-prefixes=\"['https://', '/']\"\n    :allowed-image-prefixes=\"['https://cdn.myapp.com/']\"\n    default-origin=\"https://myapp.com\"\n/\u003e\n```\n\nRelative links (e.g. `/about`) will resolve against `defaultOrigin` then be validated.\n\n### 15.6 Streaming From a Server Route (SSE Style)\n\nCreate a route that emits partial Markdown pieces:\n\n```ts\n// server/api/chat.get.ts\nexport default defineEventHandler(async (event) =\u003e {\n    const encoder = new TextEncoder();\n    const parts = [\n        '# Chat Log\\n',\n        '\\nHello **world',\n        '** from',\n        ' streamed',\n        ' markdown.',\n    ];\n    const stream = new ReadableStream({\n        start(controller) {\n            let i = 0;\n            const tick = () =\u003e {\n                if (i \u003c parts.length) {\n                    controller.enqueue(encoder.encode(parts[i++]));\n                    setTimeout(tick, 300);\n                } else controller.close();\n            };\n            tick();\n        },\n    });\n    setHeader(event, 'Content-Type', 'text/plain; charset=utf-8');\n    return stream; // Nuxt will send as a stream\n});\n```\n\n### 15.7 Client Composable to Consume Streaming Markdown\n\n```ts\n// composables/useStreamedMarkdown.ts\nimport { ref } from 'vue';\nimport { parseBlocks, parseIncompleteMarkdown } from 'streamdown-vue';\n\nexport function useStreamedMarkdown(url: string) {\n    const rendered = ref('');\n    const raw = ref('');\n\n    const start = async () =\u003e {\n        const res = await fetch(url);\n        const reader = res.body!.getReader();\n        let buf = '';\n        const decoder = new TextDecoder();\n        while (true) {\n            const { value, done } = await reader.read();\n            if (done) break;\n            buf += decoder.decode(value, { stream: true });\n            // repair, split, join\n            const repaired = parseIncompleteMarkdown(buf);\n            rendered.value = parseBlocks(repaired).join('');\n            raw.value = buf;\n        }\n    };\n\n    return { rendered, raw, start };\n}\n```\n\n### 15.8 Streaming Page Example\n\n```vue\n\u003c!-- pages/stream.vue --\u003e\n\u003ctemplate\u003e\n    \u003cbutton @click=\"start\" class=\"border px-3 py-1 mb-4\"\u003eStart Stream\u003c/button\u003e\n    \u003cStreamMarkdown :content=\"rendered\" class=\"prose\" /\u003e\n\u003c/template\u003e\n\u003cscript setup lang=\"ts\"\u003e\nimport { StreamMarkdown } from 'streamdown-vue';\nimport { useStreamedMarkdown } from '@/composables/useStreamedMarkdown';\nconst { rendered, start } = useStreamedMarkdown('/api/chat');\n\u003c/script\u003e\n```\n\n### 15.9 SSR Caveats\n\n-   The stream loop runs only client-side; on first SSR render you may want a placeholder skeleton.\n-   Shiki highlighting of large code blocks happens client-side; if you need critical highlighted code for SEO, pre-process the markdown on the server and send the HTML (future enhancement: server highlight hook).\n-   Ensure Mermaid is only executed client-side (the provided plugin pattern handles this since the component executes render logic on mount).\n\n### 15.10 Troubleshooting\n\n| Symptom                            | Fix                                                                                                                          |\n| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |\n| Copy / Download button not showing | Ensure default `CodeBlock` not overridden or your custom block renders desired buttons (remove `hideCopy` / `hideDownload`). |\n| Links stripped                     | Adjust `allowed-link-prefixes` / set `default-origin` to resolve relative paths first.                                       |\n| Images missing                     | Add CDN prefix to `allowed-image-prefixes`.                                                                                  |\n| Flash of unstyled math             | Confirm KaTeX CSS loaded in client plugin before first render.                                                               |\n| High CPU on huge streams           | Throttle updates (wrap repair/render in `requestAnimationFrame` or batch by char count).                                     |\n\nThat’s it—Nuxt integration is essentially drop‑in plus an optional streaming composable.\n\n---\n\n## 16. Recipe Gallery\n\n| Goal                                  | Snippet                                                     |\n| ------------------------------------- | ----------------------------------------------------------- |\n| AI Chat                               | Combine streaming buffer + `\u003cStreamMarkdown\u003e` (tutorial §4) |\n| Restrict to CDN images                | Set `:allowed-image-prefixes`                               |\n| Override `\u003ctable\u003e` style              | `:components=\"{ table: MyTable }\"`                          |\n| Add custom remark plugin              | `:remark-plugins=\"[myRemark]\"`                              |\n| Append footer paragraph automatically | remark plugin injecting node                                |\n| Basic local Vue example               | See `examples/basic` in repo                                |\n\nCustom remark plugin skeleton:\n\n```ts\nconst remarkAppend = () =\u003e (tree: any) =\u003e {\n    tree.children.push({ type: 'paragraph', children: [{ type: 'text', value: 'Tail note.' }] });\n};\n\u003cStreamMarkdown :remark-plugins=\"[remarkAppend]\" /\u003e\n```\n\n---\n\n## 17. FAQ\n\n**Why repair outside instead of inside the component?** Control \u0026 transparency. You can decide when to re-render; the component focuses on a deterministic AST transform.\n\n**Can I disable KaTeX or Mermaid?** For now they are bundled if you use their fences. Future option could allow toggling; PRs welcome.\n\n**Does it sanitize HTML?** Inline HTML is not allowed (passed through remark/rehype with `allowDangerousHtml: false`). Add a sanitizer plugin if you purposely enable raw HTML.\n\n**Dark mode highlighting?** Use the dual theme format: `:shiki-theme=\"{ light: 'github-light', dark: 'github-dark' }\"` for instant CSS-based theme switching without re-renders, or pass a single theme string for traditional reactive theme switching.\n\n---\n\n## 18. Development \u0026 Contributing\n\n```bash\nbun install\nbun test          # run tests (fast)\nbun run build     # build library (types + bundles)\n```\n\nPRs for: improved matrix handling, plugin toggles, directive support, performance instrumentation are appreciated.\n\n---\n\n## License\n\n[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n\nLicensed under the Apache License, Version 2.0.\n\n---\n\n### At a Glance – Minimal Streaming Loop\n\n```ts\nlet buffer = '';\nfor await (const chunk of stream) {\n    buffer += chunk;\n    buffer = parseIncompleteMarkdown(buffer);\n    const blocks = parseBlocks(buffer);\n    state.markdown = blocks.join('');\n}\n```\n\nHappy streaming! 🚀\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaluana%2Fstreamdown-vue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaluana%2Fstreamdown-vue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaluana%2Fstreamdown-vue/lists"}