feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Advanced parser hooks & low-level customization
|
||||
|
||||
This page is for low-level parser work: token transforms, AST reshaping, and custom node pipelines.
|
||||
|
||||
If you only need one of these common tasks, start elsewhere first:
|
||||
|
||||
- Replace a built-in renderer such as `image`, `code_block`, or `mermaid`: [Override Built-in Components](/guide/component-overrides)
|
||||
- Support trusted tags such as `thinking`: [Custom Tags & Advanced Components](/guide/custom-components)
|
||||
- Tune behavior without replacing renderers: [Props & Options](/guide/props)
|
||||
|
||||
## parseOptions
|
||||
`parseOptions` can be passed to `MarkdownRender` or used directly with `parseMarkdownToStructure`.
|
||||
|
||||
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — mutate tokens immediately after the `markdown-it` parse
|
||||
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — further token transforms
|
||||
|
||||
If you need to reshape the AST, post-process the returned `ParsedNode[]` and pass it to `MarkdownRender` via the `nodes` prop.
|
||||
|
||||
### Example: custom HTML-like tags (recommended)
|
||||
For simple custom tags like `<thinking>...</thinking>`, you no longer need to normalize the source or rewrite tokens. Just opt the tag into the allowlist and register a component:
|
||||
|
||||
```ts
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
import ThinkingNode from './ThinkingNode.vue'
|
||||
|
||||
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||
```
|
||||
|
||||
```vue
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:custom-html-tags="['thinking']"
|
||||
:content="markdown"
|
||||
/>
|
||||
```
|
||||
|
||||
When `custom-html-tags` includes a tag name, the parser:
|
||||
- suppresses streaming mid‑states until `<tag ...>` is complete,
|
||||
- emits a `CustomComponentNode` with `type: 'thinking'` (the tag name, no angle brackets), plus `content`, optional `attrs`, and `loading/autoClosed` flags.
|
||||
|
||||
## Custom component parsing
|
||||
|
||||
The built‑in custom tag pipeline above handles most “component‑like” tags (inline or block).
|
||||
Hooks are still useful when you need to reshape the node — for example, to strip wrappers, merge adjacent blocks, or map attributes:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
|
||||
function preTransformTokens(tokens: MarkdownToken[]) {
|
||||
return tokens.map((t) => {
|
||||
if (t.type === 'html_block' && t.tag === 'thinking')
|
||||
return { ...t, content: String(t.content ?? '').replace(/<\/?thinking[^>]*>/g, '').trim() }
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { preTransformTokens })
|
||||
```
|
||||
|
||||
Alternative flows (pick what fits your pipeline):
|
||||
- Have the backend split `thinking` into its own field/type; render `thinking` with one `MarkdownRender` and the remaining content with another, so the parser never sees raw custom HTML inline.
|
||||
- Replace custom blocks with placeholders before parsing: capture `<thinking>...</thinking>` via regex, render the cleaned body with `MarkdownRender`, then swap placeholders back to your custom component. When `thinking` is always at the top, you can also slice the head section out for dedicated rendering.
|
||||
|
||||
## setCustomComponents(id, mapping)
|
||||
- Use `setCustomComponents('docs', { thinking: ThinkingComponent })` to scope to `MarkdownRender` instances with `custom-id="docs"`.
|
||||
- Call `removeCustomComponents` to clean up mappings and avoid memory leaks in single-page apps.
|
||||
|
||||
## Scoped example
|
||||
```vue
|
||||
<MarkdownRender content="..." custom-id="docs" />
|
||||
// In setup
|
||||
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||
```
|
||||
|
||||
Advanced hooks are a powerful way to add domain-specific grammar to Markdown without changing the core parser.
|
||||
|
||||
### Typewriter prop
|
||||
|
||||
`MarkdownRender` accepts a `typewriter` boolean prop which controls whether non-`code_block` nodes use subtle fade treatment. New nodes still get the small enter transition, and when streamed text grows in place the newly appended text fragment replays a short fade instead of dimming the whole node container. This is useful for demo UIs but may be undesirable in SSR or print/export flows where deterministic output is needed.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<MarkdownRender :content="markdown" :typewriter="false" />
|
||||
```
|
||||
|
||||
CSS variables: `--typewriter-fade-duration` and `--typewriter-fade-ease` tune the initial enter transition; `--stream-update-fade-duration` and `--stream-update-fade-ease` tune the short replayed fade used for newly appended streamed text. When the stream-specific variables are not set, the appended-text fade falls back to the same duration and easing as `typewriter`.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
By default, `getMarkdown` uses English text for UI elements (e.g., "Copy" button in code blocks). You can customize these texts by providing an `i18n` option:
|
||||
|
||||
**Using a translation map:**
|
||||
|
||||
```ts
|
||||
import { getMarkdown } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown('editor-1', {
|
||||
i18n: {
|
||||
'common.copy': '复制',
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Using a translation function:**
|
||||
|
||||
```ts
|
||||
import { getMarkdown } from 'markstream-vue'
|
||||
import { useI18n } from 'vue-i18n' // or any i18n library
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const md = getMarkdown('editor-1', {
|
||||
i18n: (key: string) => t(key)
|
||||
})
|
||||
```
|
||||
|
||||
**Overriding default texts globally:**
|
||||
|
||||
If you just want to change some default UI texts (e.g., "Copy" → "复制") without setting up full i18n, use `setDefaultI18nMap` to replace the fallback translations:
|
||||
|
||||
```ts
|
||||
import { setDefaultI18nMap } from 'markstream-vue'
|
||||
|
||||
setDefaultI18nMap({
|
||||
'common.copy': '复制',
|
||||
'common.copied': '已复制',
|
||||
'common.decrease': '减少',
|
||||
'common.reset': '重置',
|
||||
'common.increase': '增加',
|
||||
'common.expand': '展开',
|
||||
'common.collapse': '收起',
|
||||
'common.preview': '预览',
|
||||
'common.source': '源码',
|
||||
'common.export': '导出',
|
||||
'common.open': '打开',
|
||||
'common.zoomIn': '放大',
|
||||
'common.zoomOut': '缩小',
|
||||
'common.resetZoom': '重置缩放',
|
||||
'image.loadError': '图片加载失败',
|
||||
'image.loading': '图片加载中...',
|
||||
})
|
||||
```
|
||||
|
||||
This is useful when:
|
||||
- You don't need language switching but prefer different wording
|
||||
- You want Chinese (or other language) UI by default
|
||||
- You're not using `vue-i18n` but still want custom UI texts
|
||||
|
||||
**Default translations:**
|
||||
|
||||
- `common.copy`: "Copy" — Used in code block copy buttons
|
||||
- `common.copied`: "Copied" — Shown after copying
|
||||
- `image.loadError`: "Image failed to load" — Image fallback text
|
||||
- `image.loading`: "Loading image..." — Shown while loading
|
||||
|
||||
This design keeps the markdown utilities pure and free from global side effects, allowing you to integrate with any i18n solution or provide static translations.
|
||||
|
||||
Try this — minimal example using parseOptions and a custom component registration:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure, setCustomComponents } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown()
|
||||
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||
const nodes = parseMarkdownToStructure('[[CUSTOM:1]]', md)
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
description: Build AI chat and streaming Markdown UIs with nodes plus final patterns, performance guidance, trusted custom tags, and SSR-safe setup.
|
||||
---
|
||||
|
||||
# AI Chat & Streaming
|
||||
|
||||
Use this path when you are building a chat UI, token stream, SSE response viewer, or any screen where Markdown updates frequently while the user is watching.
|
||||
|
||||
If you only render static articles or docs pages, go back to [Usage & Streaming](/guide/usage) and prefer the simpler `content` path.
|
||||
|
||||
## 1. Choose the leanest install
|
||||
|
||||
| Need | Packages | Best for |
|
||||
| --- | --- | --- |
|
||||
| Text-only or lightweight chat UI | `markstream-vue` | Basic Markdown, lists, links, blockquotes |
|
||||
| Syntax-highlighted code without Monaco | `markstream-vue stream-markdown` | SSR-friendly transcripts, lower bundle budgets |
|
||||
| Rich code interactions | `markstream-vue stream-monaco` | Copy, preview, diff, and Monaco-powered code blocks |
|
||||
| Diagrams or math in chat output | `markstream-vue mermaid katex` | Mermaid fences and KaTeX formulas |
|
||||
|
||||
Install only the peers you actually expect to show up in responses.
|
||||
|
||||
## 2. Recommended data flow
|
||||
|
||||
For frequent updates, keep parsing outside `MarkdownRender` and pass `nodes` + `final`.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender, { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const streamedText = ref('')
|
||||
const final = ref(false)
|
||||
const md = getMarkdown('chat-message')
|
||||
|
||||
const nodes = computed(() =>
|
||||
parseMarkdownToStructure(streamedText.value, md, { final: final.value }),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="chat"
|
||||
:nodes="nodes"
|
||||
:final="final"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
Why this path works better:
|
||||
|
||||
- `MarkdownRender` does not need to reparse the full string on every tiny token update.
|
||||
- You can move parsing into a store, worker, or message pipeline later without changing the renderer contract.
|
||||
- `custom-id="chat"` gives you a scoped place to theme the chat surface or override one renderer safely.
|
||||
|
||||
## 3. Renderer settings that usually work well
|
||||
|
||||
- Keep the default virtualization behavior for long transcripts. Only tune `maxLiveNodes` if you have a measured reason.
|
||||
- Use `renderCodeBlocksAsPre` when code fences appear often but Monaco is too heavy for your chat surface.
|
||||
- Leave heavy peers off until you need them. Chat UIs get a large win from not shipping Mermaid, KaTeX, or Monaco by default.
|
||||
- If you disable virtualization (`:max-live-nodes="0"`), then the batching props in [Props & Options](/guide/props) become more important.
|
||||
|
||||
## 4. Common upgrade paths
|
||||
|
||||
### Better code blocks
|
||||
|
||||
- Want a lighter docs-style look: use `MarkdownCodeBlockNode` with `stream-markdown`
|
||||
- Want richer preview/diff controls: use `CodeBlockNode` with `stream-monaco`
|
||||
|
||||
See [Renderer & Node Components](/guide/components) for the trade-offs.
|
||||
|
||||
### Trusted tags such as `thinking`
|
||||
|
||||
Use `custom-html-tags` plus `setCustomComponents('chat', mapping)` so custom tags only affect the chat surface.
|
||||
|
||||
See [Custom Tags & Advanced Components](/guide/custom-components).
|
||||
|
||||
### Scoped overrides for one message surface
|
||||
|
||||
Use `setCustomComponents('chat', { image: ChatImageNode })` and render with `custom-id="chat"`.
|
||||
|
||||
See [Override Built-in Components](/guide/component-overrides).
|
||||
|
||||
## 5. CSS and SSR checklist
|
||||
|
||||
- Load your reset first, then import `markstream-vue/index.css` inside `@layer components`.
|
||||
- Import `katex/dist/katex.min.css` only if math is enabled.
|
||||
- Gate browser-only peers such as Mermaid, D2, or Monaco behind client-only boundaries in SSR setups.
|
||||
- If styles leak, scope your chat tweaks under `[data-custom-id="chat"]`.
|
||||
|
||||
Start here when visuals look wrong: [Troubleshooting](/guide/troubleshooting#css-looks-wrong-start-here)
|
||||
|
||||
## 6. When not to use this path
|
||||
|
||||
- Use `content` when updates are infrequent or the page is basically static.
|
||||
- Use server-side preparse + `nodes` when another layer already owns Markdown parsing.
|
||||
- Use the framework-specific guides when SSR/runtime rules matter more than streaming itself.
|
||||
|
||||
## Next pages
|
||||
|
||||
- [Installation](/guide/installation) for peer decisions
|
||||
- [Usage & Streaming](/guide/usage) for `content` vs `nodes`
|
||||
- [Performance](/guide/performance) for larger transcripts
|
||||
- [Renderer & Node Components](/guide/components) for code/math/diagram component choices
|
||||
- [Troubleshooting](/guide/troubleshooting) when CSS, peers, or SSR behave unexpectedly
|
||||
@@ -0,0 +1,178 @@
|
||||
# AI / Skills Workflows
|
||||
|
||||
If you use Codex, Cursor, Claude Code, ChatGPT, or another coding assistant to adopt markstream, the fastest way to get useful output is to give the tool the right constraints up front.
|
||||
|
||||
This page is intentionally practical: copyable prompts, rollout checklists, and what a reusable markstream-focused skill should cover.
|
||||
|
||||
## Start with your goal
|
||||
|
||||
- Want packaged skills you can install right now: jump to [Install the packaged skills](#install-the-packaged-skills)
|
||||
- Want ready-made prompt templates without cloning the repo: jump to [Copyable prompts](#2-copyable-prompts)
|
||||
- Want to migrate from another renderer with an assistant: start with [Copyable prompts](#2-copyable-prompts) and the rollout pattern below
|
||||
- Want to version reusable AI assets inside your own repo: read [Repository assets](#repository-assets)
|
||||
|
||||
## Install the packaged skills
|
||||
|
||||
For end users, the recommended command is the shared `skills` installer:
|
||||
|
||||
```bash
|
||||
npx skills add Simon-He95/markstream-vue
|
||||
```
|
||||
|
||||
That path works because the repository now publishes reusable skills under `.agents/skills`, which `npx skills` can discover directly from GitHub.
|
||||
|
||||
The Vercel `skills` installer also supports multiple source formats:
|
||||
|
||||
```bash
|
||||
# GitHub shorthand (owner/repo)
|
||||
npx skills add Simon-He95/markstream-vue
|
||||
|
||||
# Full GitHub URL
|
||||
npx skills add https://github.com/Simon-He95/markstream-vue
|
||||
|
||||
# Direct path to one skill in this repo
|
||||
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
|
||||
|
||||
# Any git URL
|
||||
npx skills add git@github.com:Simon-He95/markstream-vue.git
|
||||
```
|
||||
|
||||
If you specifically want the package CLI, you can still use:
|
||||
|
||||
```bash
|
||||
npx markstream-vue skills install
|
||||
# or
|
||||
pnpm dlx markstream-vue skills install
|
||||
```
|
||||
|
||||
Useful variants:
|
||||
|
||||
```bash
|
||||
npx markstream-vue skills list
|
||||
npx markstream-vue skills install --target codex
|
||||
npx markstream-vue skills install --target ./tmp/skills-test --mode copy --force
|
||||
```
|
||||
|
||||
- `npx skills add Simon-He95/markstream-vue` is the primary recommendation for Codex-compatible skill installation
|
||||
- `skills install` defaults to `copy`, which is safer for `npx` and `dlx` usage
|
||||
- the default target is `~/.agents/skills`
|
||||
- `--target codex` targets `${CODEX_HOME:-~/.codex}/skills`
|
||||
- `--target` accepts either `agents`, `codex`, or any custom path
|
||||
- `--force` replaces existing installs at the target path
|
||||
|
||||
The prompts stay in the repository under `prompts/`; the packaged installer only handles the skill folders.
|
||||
|
||||
The same CLI can also expose bundled prompt templates:
|
||||
|
||||
```bash
|
||||
npx markstream-vue prompts list
|
||||
npx markstream-vue prompts show install-markstream
|
||||
```
|
||||
|
||||
That is usually the easiest way for npm users to discover the maintained prompt set without cloning the repository.
|
||||
|
||||
## Repository assets
|
||||
|
||||
This repository now includes reusable assets you can version with the codebase:
|
||||
|
||||
- Task skills: `markstream-install`, `markstream-custom-components`, `markstream-migration`
|
||||
- Framework entry skills: `markstream-vue`, `markstream-nuxt`, `markstream-vue2`, `markstream-vue2-cli`, `markstream-vue2-vite`, `markstream-react`, `markstream-angular`
|
||||
- `prompts/install-markstream.md`
|
||||
- `prompts/override-built-in-components.md`
|
||||
- `prompts/add-custom-tag-thinking.md`
|
||||
- `prompts/migrate-react-markdown.md`
|
||||
- `prompts/audit-markstream-integration.md`
|
||||
|
||||
Use `.agents/skills/` when you want reusable Codex workflow assets that GitHub-based installers can discover automatically. The task skills cover cross-framework work; the framework entry skills make Vue 3, Nuxt, Vue 2, Vue 2 CLI, Vue 2 Vite, React, and Angular scenarios directly discoverable. Use `prompts/` when you want copyable starting prompts for humans or other assistants.
|
||||
|
||||
## 1. Give the AI these five facts first
|
||||
|
||||
Before you ask for code changes, include:
|
||||
|
||||
- framework and version, for example Vue 3, Nuxt 3, React 18, or Angular 20
|
||||
- CSS stack, for example Tailwind, UnoCSS, reset libraries, or a design system
|
||||
- rendering mode: static article, docs site, SSR, or streaming chat
|
||||
- required peers: Monaco, Mermaid, D2, KaTeX, or Shiki
|
||||
- whether overrides must stay scoped to one area of the app
|
||||
|
||||
Without that context, assistants often install the wrong peers or place CSS in the wrong order.
|
||||
|
||||
## 2. Copyable prompts
|
||||
|
||||
### Install markstream in an existing Vue 3 app
|
||||
|
||||
```text
|
||||
Install markstream-vue into this Vue 3 app.
|
||||
Use the smallest peer-dependency set that matches these needs: [fill in Monaco / Mermaid / D2 / KaTeX / Shiki].
|
||||
Keep CSS order safe with my existing stack: [fill in Tailwind / UnoCSS / reset library].
|
||||
Add one minimal render example, and explain whether I should use `content` or `nodes`.
|
||||
If you replace or override anything, keep it scoped with `custom-id`.
|
||||
```
|
||||
|
||||
### Migrate from `react-markdown`
|
||||
|
||||
```text
|
||||
Migrate this React codebase from react-markdown to markstream-react.
|
||||
Audit which remark/rehype behaviors have no direct 1:1 equivalent.
|
||||
Replace simple renderer overrides first, then call out anything that needs custom nodes, customHtmlTags, or parser/node post-processing.
|
||||
Preserve existing user-visible behavior where practical.
|
||||
```
|
||||
|
||||
### Add a custom tag such as `thinking`
|
||||
|
||||
```text
|
||||
Add support for a trusted custom Markdown tag named `thinking`.
|
||||
Use `customHtmlTags` plus a scoped `setCustomComponents` mapping.
|
||||
Render nested Markdown inside the custom component, and keep the implementation streaming-friendly.
|
||||
Do not use global overrides unless there is a clear reason.
|
||||
```
|
||||
|
||||
### Debug styling or SSR issues
|
||||
|
||||
```text
|
||||
Audit this markstream integration for CSS order, reset conflicts, optional peers, and SSR-only problems.
|
||||
Check whether `markstream-vue/index.css` is loaded in the correct layer, whether KaTeX CSS is missing, and whether browser-only peers are gated behind client-only boundaries.
|
||||
Explain the smallest fix set.
|
||||
```
|
||||
|
||||
## 3. What a reusable markstream skill should do
|
||||
|
||||
If you are building a reusable prompt, template, or coding skill around markstream, it should reliably cover these steps:
|
||||
|
||||
- detect the framework and package manager
|
||||
- install only the required peers for the requested features
|
||||
- place CSS after resets and inside `@layer components` when utility frameworks are involved
|
||||
- choose `content` for simple rendering and `nodes` plus `final` for streaming
|
||||
- use `custom-id` for overrides by default
|
||||
- point the user to the right docs page after making changes
|
||||
|
||||
Good follow-up links for a skill to cite:
|
||||
|
||||
- [Installation](/guide/installation)
|
||||
- [Usage & Streaming](/guide/usage)
|
||||
- [Override Built-in Components](/guide/component-overrides)
|
||||
- [Custom Tags & Advanced Components](/guide/custom-components)
|
||||
- [Troubleshooting](/guide/troubleshooting)
|
||||
|
||||
## 4. Point AI agents at the agent context file
|
||||
|
||||
For repository-aware assistants, also point them to:
|
||||
|
||||
- [AI / LLM context](/llms)
|
||||
|
||||
That file is agent-facing. This page is human-facing. In practice, the best results come from giving the assistant both:
|
||||
|
||||
- the task-specific prompt from this page
|
||||
- the repository context file from `/llms`
|
||||
|
||||
## 5. A rollout pattern that works well
|
||||
|
||||
When the task is bigger than a one-file demo, ask the assistant to work in this order:
|
||||
|
||||
1. install and verify peers
|
||||
2. make the base renderer work
|
||||
3. fix CSS order
|
||||
4. add scoped overrides or custom tags
|
||||
5. run tests or docs build
|
||||
|
||||
That sequence prevents most “it compiled but looks wrong” outcomes.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Angular Installation
|
||||
|
||||
Install `markstream-angular` into your Angular 20+ project.
|
||||
|
||||
```bash
|
||||
pnpm add markstream-angular @angular/core @angular/common
|
||||
```
|
||||
|
||||
## Required CSS
|
||||
|
||||
Import the renderer stylesheet once in your app entry:
|
||||
|
||||
```ts
|
||||
import 'markstream-angular/index.css'
|
||||
```
|
||||
|
||||
If you enable math rendering, also import:
|
||||
|
||||
```ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
## Optional Peer Dependencies
|
||||
|
||||
Install only the heavy features you need:
|
||||
|
||||
| Feature | Package |
|
||||
| --- | --- |
|
||||
| Monaco code blocks | `stream-monaco` |
|
||||
| Mermaid diagrams | `mermaid` |
|
||||
| KaTeX math | `katex` |
|
||||
| D2 diagrams | `@terrastruct/d2` |
|
||||
| AntV infographic blocks | `@antv/infographic` |
|
||||
|
||||
Quick install:
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco mermaid katex @terrastruct/d2 @antv/infographic
|
||||
```
|
||||
|
||||
## Optional Workers
|
||||
|
||||
`markstream-angular` exports the same worker entry points used by the React / Vue integrations:
|
||||
|
||||
```ts
|
||||
import { setKaTeXWorker, setMermaidWorker } from 'markstream-angular'
|
||||
import KatexWorker from 'markstream-angular/workers/katexRenderer.worker?worker'
|
||||
import MermaidWorker from 'markstream-angular/workers/mermaidParser.worker?worker'
|
||||
|
||||
setKaTeXWorker(new KatexWorker())
|
||||
setMermaidWorker(new MermaidWorker())
|
||||
```
|
||||
|
||||
## Local Playground
|
||||
|
||||
Inside this monorepo, the Angular playground routes are:
|
||||
|
||||
- `/` for the main streaming demo
|
||||
- `/test` for the regression lab
|
||||
- `/test-sandbox` for isolated framework/version rendering
|
||||
|
||||
Run it locally with:
|
||||
|
||||
```bash
|
||||
pnpm play:angular
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Angular Quick Start
|
||||
|
||||
Get `markstream-angular` rendering markdown in a standalone Angular app.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { Component, signal } from '@angular/core'
|
||||
import { bootstrapApplication } from '@angular/platform-browser'
|
||||
import { MarkstreamAngularComponent } from 'markstream-angular'
|
||||
import 'markstream-angular/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [MarkstreamAngularComponent],
|
||||
template: `
|
||||
<markstream-angular
|
||||
[content]="markdown()"
|
||||
[final]="true"
|
||||
[codeBlockStream]="true"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
class AppComponent {
|
||||
readonly markdown = signal(`# Hello Angular
|
||||
|
||||
This is **markstream-angular**.
|
||||
|
||||
\`\`\`ts
|
||||
export function greet(name: string) {
|
||||
return \`hello \${name}\`
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart LR
|
||||
Input --> Parser --> Renderer
|
||||
\`\`\`
|
||||
|
||||
$E = mc^2$
|
||||
`)
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent)
|
||||
```
|
||||
|
||||
## Streaming Example
|
||||
|
||||
```ts
|
||||
import { Component, signal } from '@angular/core'
|
||||
import { MarkstreamAngularComponent } from 'markstream-angular'
|
||||
|
||||
@Component({
|
||||
selector: 'app-streaming-demo',
|
||||
standalone: true,
|
||||
imports: [MarkstreamAngularComponent],
|
||||
template: `
|
||||
<markstream-angular
|
||||
[content]="content()"
|
||||
[final]="done()"
|
||||
[codeBlockStream]="true"
|
||||
[batchRendering]="true"
|
||||
[typewriter]="true"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
export class StreamingDemoComponent {
|
||||
readonly content = signal('# Partial output')
|
||||
readonly done = signal(false)
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Props
|
||||
|
||||
```ts
|
||||
import type { CodeBlockMonacoOptions, NodeRendererProps } from 'markstream-angular'
|
||||
|
||||
const monacoOptions: CodeBlockMonacoOptions = {
|
||||
fontSize: 13,
|
||||
wordWrap: 'on',
|
||||
}
|
||||
|
||||
const props: NodeRendererProps = {
|
||||
content: '# Angular',
|
||||
codeBlockMonacoOptions: monacoOptions,
|
||||
final: true,
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Tags
|
||||
|
||||
The Angular package supports the same custom HTML tag flow used in the playground `thinking` demos:
|
||||
|
||||
```html
|
||||
<markstream-angular
|
||||
[content]="markdown()"
|
||||
[customHtmlTags]="['thinking']"
|
||||
[customComponents]="customComponents"
|
||||
/>
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Angular Installation](/guide/angular-installation)
|
||||
- [Guide Index](/guide/)
|
||||
- [Playground Guide](/guide/playground)
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
description: Reference the low-level markstream-vue API for parser helpers, render-pipeline decisions, scoped overrides, and AST-aware integration points.
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
This page covers the low-level entry points behind `markstream-vue`: parser helpers, render-pipeline decisions, and scoping hooks.
|
||||
|
||||
If you are trying to look up exported Vue components such as `MarkdownRender`, `CodeBlockNode`, or `ImageNode`, use [Renderer & Node Components](/guide/components) instead. Pair this page with [Usage](/guide/usage) and [Props](/guide/props) when wiring everything together.
|
||||
|
||||
## Render pipeline at a glance
|
||||
|
||||
```
|
||||
Markdown string → getMarkdown() → markdown-it-ts instance
|
||||
↓
|
||||
parseMarkdownToStructure(content, md) → AST (BaseNode[])
|
||||
↓
|
||||
<MarkdownRender> → node components (CodeBlockNode, ImageNode, …)
|
||||
```
|
||||
|
||||
You can jump in at any stage:
|
||||
- Provide `content` to let the component handle parsing automatically.
|
||||
- Provide `nodes` when you need full control over the AST (server-side parsing, custom transforms).
|
||||
|
||||
## Parser helpers
|
||||
|
||||
| Helper | Purpose | When to use |
|
||||
| ------ | ------- | ----------- |
|
||||
| `getMarkdown(msgId?, options?)` | Returns a configured `markdown-it-ts` instance with the plugins this package expects. | Customize parser options (HTML toggles, additional plugins) before transforming tokens. |
|
||||
| `parseMarkdownToStructure(content, md)` | Generates the AST consumed by `MarkdownRender` from a Markdown string. | Pre-parse on the server, run validations, or reuse the AST across renders. |
|
||||
|
||||
Both helpers are framework-agnostic and can run in Node or the browser. For large documents you can reuse the `md` instance between parses to avoid re-initializing plugins.
|
||||
|
||||
## Custom components & scoping
|
||||
|
||||
Use `setCustomComponents(customId?, mapping)` to override any node renderer. Pair it with the `custom-id` prop on `MarkdownRender` so replacements stay scoped.
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const CustomImageNode: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
image: CustomImageNode,
|
||||
})
|
||||
```
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = ''
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender custom-id="docs" :content="md" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Tips:
|
||||
- Use descriptive IDs (`docs`, `playground`, `pdf-export`) for tracing.
|
||||
- Call `setCustomComponents(mapping)` to set globals, but prefer scoped IDs to avoid surprises in multi-instance apps.
|
||||
- Clean up mappings in SPA routers if you register them dynamically.
|
||||
|
||||
## Parse hooks & transforms
|
||||
|
||||
When passing `content`, you can intercept parser stages through `parse-options` (prop) or the `ParseOptions` parameter of `parseMarkdownToStructure`.
|
||||
|
||||
Hooks:
|
||||
- `preTransformTokens(tokens)` — mutate tokens before default handling.
|
||||
- `postTransformTokens(tokens)` — inspect/adjust tokens before node generation.
|
||||
|
||||
If you need to reshape the AST, post-process the returned nodes and pass them to `MarkdownRender` via `nodes`.
|
||||
|
||||
Example: render AI “thinking” tags as custom components (no hooks needed):
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const ThinkingNode: Component
|
||||
|
||||
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||
```
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const doc = '<thinking>Need a plan</thinking>'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:custom-html-tags="['thinking']"
|
||||
:content="doc"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
Hooks remain useful if you want to reshape the emitted `thinking` node (strip wrappers, remap attrs, merge blocks, etc.), or post-process the parsed nodes before rendering.
|
||||
|
||||
## Utility exports
|
||||
|
||||
Besides the core renderer and parser helpers, the package exposes:
|
||||
|
||||
- `CodeBlockNode`, `MarkdownCodeBlockNode`, `MermaidBlockNode`, `MathBlockNode`, `ImageNode`, etc. — see [Components](/guide/components) for their props and CSS requirements.
|
||||
- `VueRendererMarkdown` (global component plugin) and shared type exports (component prop interfaces, parser types).
|
||||
|
||||
For parser types and hooks, see [/guide/parser-api](/guide/parser-api) (or the `stream-markdown-parser` README on npm).
|
||||
|
||||
## Styling + troubleshooting reminders
|
||||
|
||||
- Always include a reset before `markstream-vue/index.css` and wrap it with `@layer components` when using Tailwind or UnoCSS. See the [Tailwind guide](/guide/tailwind) and the [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here).
|
||||
- Code/graph peers: KaTeX needs its own CSS import; Mermaid does not. Missing KaTeX styles often manifest as invisible formulas.
|
||||
- Use `custom-id` to scope overrides and avoid global selector conflicts.
|
||||
|
||||
Need more examples? Jump into the [Playground](/guide/playground) or run `pnpm play` locally to experiment with custom parsers and renderers.
|
||||
@@ -0,0 +1,268 @@
|
||||
# CodeBlockNode (Component)
|
||||
|
||||
`CodeBlockNode` 是库中用于渲染富交互代码块的组件。对于需要编辑/高亮/增量渲染的场景,推荐安装 `stream-monaco`。组件为头部提供灵活的自定义点(props + slots),并在常见场景下提供可拦截的事件。
|
||||
|
||||
## Quick summary
|
||||
- Monaco mode (install `stream-monaco`) — editor-like rendering with workers
|
||||
- Fallback — plain `<pre><code>` when `stream-monaco` is not installed
|
||||
- If you want Shiki-based highlighting (no Monaco), use `MarkdownCodeBlockNode` (peer: `stream-markdown`)
|
||||
|
||||
## Props
|
||||
Refer to `src/types/component-props.ts` for full signature. Key props:
|
||||
- `node` — code_block node (required)
|
||||
- `loading`, `stream`, `isShowPreview`
|
||||
- `monacoOptions` — typed as `CodeBlockMonacoOptions` and forwarded to `stream-monaco`
|
||||
- diff options such as `diffHideUnchangedRegions`, `diffLineStyle`, `diffAppearance`, `diffUnchangedRegionStyle`, `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` belong here
|
||||
- Header controls: `showHeader`, `showCollapseButton`, `showCopyButton`, `showExpandButton`, `showPreviewButton`, `showFontSizeButtons`, `showTooltips`
|
||||
|
||||
Default diff UX in Monaco mode:
|
||||
|
||||
- `diffHideUnchangedRegions: { enabled: true, contextLineCount: 2, minimumLineCount: 4, revealLineCount: 5 }`
|
||||
- `diffLineStyle: 'background'`
|
||||
- `diffAppearance: 'auto'`
|
||||
- `diffUnchangedRegionStyle: 'line-info'`
|
||||
- `diffHunkActionsOnHover: true`
|
||||
- `diffHunkHoverHideDelayMs: 160`
|
||||
|
||||
You can override any of them through `monacoOptions`.
|
||||
When the preset uses `diffAppearance: 'auto'`, `CodeBlockNode` resolves it to the current light/dark surface before passing the options to `stream-monaco`.
|
||||
|
||||
Diff blocks also show `- / +` line counts in the built-in header.
|
||||
|
||||
## Slots
|
||||
- `header-left` — replace left header
|
||||
- `header-right` — replace right header
|
||||
- `loading` — customize placeholder when streaming is disabled
|
||||
|
||||
## Emits
|
||||
- `copy(text: string)` — when copy pressed
|
||||
- `previewCode(payload)` — only emitted when you attach a `@preview-code` listener; payload is `{ node, artifactType, artifactTitle, id }`
|
||||
|
||||
## Examples
|
||||
### Install and run (Monaco)
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
```
|
||||
|
||||
### Basic example
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'js',
|
||||
code: 'console.log(1)',
|
||||
raw: 'console.log(1)',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Replace header and hide copy button
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'js',
|
||||
code: 'console.log(1)',
|
||||
raw: 'console.log(1)',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
|
||||
function runSnippet() {}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" :show-copy-button="false">
|
||||
<template #header-left>
|
||||
<div class="flex items-center">
|
||||
Custom left
|
||||
</div>
|
||||
</template>
|
||||
<template #header-right>
|
||||
<button @click="runSnippet">
|
||||
Run
|
||||
</button>
|
||||
</template>
|
||||
</CodeBlockNode>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Custom loading placeholder
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'console.log("loading")',
|
||||
raw: 'console.log("loading")',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" :stream="false" :loading="true">
|
||||
<template #loading="{ loading, stream }">
|
||||
<div v-if="loading && !stream">
|
||||
Loading editor assets…
|
||||
</div>
|
||||
</template>
|
||||
</CodeBlockNode>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Theme Switching
|
||||
|
||||
`CodeBlockNode` supports automatic theme switching based on dark/light mode. Use `@vueuse/core`'s `useDark` composable to track the theme state and pass theme names to `MarkdownRender` or `CodeBlockNode`.
|
||||
|
||||
### Using @vueuse/core in standalone Vue apps
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const isDark = useDark() // Ref<boolean> reactive to system/theme preference
|
||||
const toggleDark = useToggle(isDark)
|
||||
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
|
||||
|
||||
// Available themes (must include the themes you want to use)
|
||||
const themes = [
|
||||
'vitesse-dark',
|
||||
'vitesse-light',
|
||||
'github-dark',
|
||||
'github-light',
|
||||
// ... more themes
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button @click="toggleDark()">
|
||||
Toggle Theme
|
||||
</button>
|
||||
<MarkdownRender
|
||||
:is-dark="isDark"
|
||||
code-block-dark-theme="vitesse-dark"
|
||||
code-block-light-theme="vitesse-light"
|
||||
:themes="themes"
|
||||
:content="content"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### VitePress integration
|
||||
|
||||
For VitePress, use the built-in `isDark` from VitePress's `useData()`:
|
||||
|
||||
```ts
|
||||
// docs/.vitepress/theme/composables/useDark.ts
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
/**
|
||||
* VitePress theme composable for dark mode
|
||||
* Uses VitePress's built-in isDark from useData()
|
||||
*/
|
||||
export function useDark() {
|
||||
const { isDark } = useData()
|
||||
return isDark
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- In any .md file or component -->
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { useDark } from '../../.vitepress/theme'
|
||||
|
||||
const isDark = useDark()
|
||||
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
|
||||
|
||||
const themes = [
|
||||
'vitesse-dark',
|
||||
'vitesse-light',
|
||||
'github-dark',
|
||||
'github-light',
|
||||
// ... more themes
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:is-dark="isDark"
|
||||
code-block-dark-theme="vitesse-dark"
|
||||
code-block-light-theme="vitesse-light"
|
||||
:themes="themes"
|
||||
:content="content"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
The `theme` prop accepts either a fixed theme or a light/dark pair:
|
||||
|
||||
```vue
|
||||
<!-- Auto-switch between light and dark (recommended) -->
|
||||
<CodeBlockNode :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" />
|
||||
|
||||
<!-- Fixed theme (ignores isDark) -->
|
||||
<CodeBlockNode theme="monokai" />
|
||||
|
||||
<!-- Theme object (fixed, ignores isDark) -->
|
||||
<CodeBlockNode :theme="{ name: 'my-theme', colors: { ... } }" />
|
||||
```
|
||||
|
||||
When using a `{ light, dark }` pair, the component automatically switches based on the `isDark` prop.
|
||||
|
||||
The `themes` prop registers the available themes so Monaco can lazy-load them on demand.
|
||||
|
||||
> **Backward compatibility:** `darkTheme` / `lightTheme` props still work but are deprecated. Prefer the unified `theme` prop.
|
||||
|
||||
**Key differences for CodeBlockNode:**
|
||||
|
||||
| Prop | Direct CodeBlockNode | Via MarkdownRender |
|
||||
|------|---------------------|-------------------|
|
||||
| `isDark` | Passed directly to `<CodeBlockNode :is-dark="isDark" />` | Passed via `<MarkdownRender :is-dark="isDark" />` and automatically forwarded |
|
||||
| Theme | `:theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }"` | `:code-block-dark-theme="'vitesse-dark'"` `:code-block-light-theme="'vitesse-light'"` (legacy) |
|
||||
| Themes list | `:themes="['vitesse-dark', 'vitesse-light', ...]"` | `:themes="['vitesse-dark', 'vitesse-light', ...]"` |
|
||||
|
||||
## Notes
|
||||
- The CodeBlock header API is documented in `docs/guide/codeblock-header.md` (examples for replacing header and custom loading placeholder).
|
||||
- `CodeBlockNode` and `MermaidBlockNode` intentionally use different `copy` event payloads: `CodeBlockNode` emits `copy(text: string)`, while `MermaidBlockNode` emits `copy(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)` (supports `preventDefault()`).
|
||||
|
||||
Try this — simple snapshot example (inline usage):
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = { type: 'code_block', language: 'js', code: 'console.log("hello")', raw: 'console.log("hello")' } satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Tell me if you want me to also:
|
||||
- Add stricter TS types for `defineEmits` in the SFC
|
||||
- Update the docs sidebar to link this page (I can add the entry to `docs/guide/index.md` or the sidebar config)
|
||||
- Add a runnable example in `playground/` demonstrating slot usage and event interception
|
||||
@@ -0,0 +1,98 @@
|
||||
# Code Block Rendering
|
||||
|
||||
## Overview
|
||||
|
||||
Code blocks can be rendered in three ways depending on which optional dependencies you install and how you configure the library:
|
||||
|
||||
- Monaco (recommended for large/interactive code blocks): installs and uses `stream-monaco` to provide an editor-like, incremental rendering experience. The library lazy-loads `stream-monaco` at runtime when available.
|
||||
- Shiki (MarkdownCodeBlockNode): install `stream-markdown` and override the `code_block` node via `setCustomComponents` to use a lightweight Markdown-driven renderer.
|
||||
- Fallback (no extra deps): if neither optional package is installed, code blocks render as plain `<pre><code>` blocks (basic styling / no Monaco features).
|
||||
|
||||
## Monaco (recommended)
|
||||
|
||||
- Install:
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
# or
|
||||
npm i stream-monaco
|
||||
```
|
||||
|
||||
- Behavior: when `stream-monaco` is present the built-in `CodeBlockNode` will use Monaco-based streaming updates for large or frequently-updated code blocks.
|
||||
|
||||
- Vite worker note: Monaco and some worker-backed features require appropriate worker bundling configuration in your bundler (Vite) so the editor/workers are available at runtime. See [/nuxt-ssr](/nuxt-ssr) for guidance and examples of configuring workers and client-only initialization.
|
||||
- See also: [/guide/monaco](/guide/monaco) for worker bundling tips and preload snippets.
|
||||
|
||||
## Shiki mode (MarkdownCodeBlockNode)
|
||||
|
||||
- Install:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
# or
|
||||
npm i stream-markdown
|
||||
```
|
||||
|
||||
- Override the `code_block` node via `setCustomComponents` to register the Shiki-based renderer:
|
||||
|
||||
```ts twoslash
|
||||
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
|
||||
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||
```
|
||||
|
||||
Once set, `MarkdownCodeBlockNode` (powered by Shiki via `stream-markdown`) will be used for `code_block` nodes. You can also supply your own component that uses `stream-markdown` directly.
|
||||
|
||||
### Language icon lazy loading
|
||||
|
||||
To keep the main bundle smaller, infrequent language icons are split into an async chunk:
|
||||
|
||||
- Common languages (JS/TS/HTML/CSS/JSON/Python/etc.) stay in the main bundle.
|
||||
- Rare languages load on demand and will update icon output automatically after the async chunk resolves.
|
||||
- If you prefer to avoid first-hit fallback icons, preload once during app idle:
|
||||
|
||||
```ts twoslash
|
||||
import { preloadExtendedLanguageIcons } from 'markstream-vue'
|
||||
|
||||
if (typeof window !== 'undefined')
|
||||
void preloadExtendedLanguageIcons()
|
||||
```
|
||||
|
||||
### Vue CLI 4 (Webpack 4) notes
|
||||
|
||||
If you use Vue CLI 4 (Webpack 4), it’s recommended to use the Shiki mode for code blocks and **override** `code_block` to avoid Monaco + legacy-bundler edge cases.
|
||||
|
||||
Key pitfalls and fixes (see `playground-vue2-cli`):
|
||||
|
||||
- Webpack 4 doesn’t support `package.json#exports` → prefer `dist/*` paths via `resolve.alias`.
|
||||
- ESM-only packages (like `stream-markdown`) may not be discoverable via `require.resolve()` inside `vue.config.js` (CJS) → use a filesystem fallback to find `node_modules/stream-markdown`, and alias it to `dist/index.js`.
|
||||
- If you use `IgnorePlugin` to skip optional deps, don’t accidentally ignore `stream-markdown` (otherwise you’ll get `webpackMissingModule` at runtime).
|
||||
|
||||
## Fallback
|
||||
|
||||
If you don't install either optional package the renderer falls back to a simple `pre`/`code` representation.
|
||||
|
||||
## Links & further reading
|
||||
|
||||
- Worker / SSR guidance: [/nuxt-ssr](/nuxt-ssr)
|
||||
- Installation notes: [/guide/installation](/guide/installation)
|
||||
|
||||
Try this — simple CodeBlock render:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'js',
|
||||
code: 'console.log(42)',
|
||||
raw: 'console.log(42)',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" />
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,150 @@
|
||||
# Code block header customization
|
||||
|
||||
The code block component exposes a flexible header API to adapt the header to your app UX:
|
||||
|
||||
- Toggle header on/off
|
||||
- Show or hide built-in toolbar buttons (collapse, copy, expand, preview, font-size)
|
||||
- Replace the left/right header content via named slots (`header-left`, `header-right`)
|
||||
|
||||
Slots
|
||||
|
||||
- `header-left` — Replace the left side of the header
|
||||
- `header-right` — Replace the right side of the header
|
||||
- `loading` — Customize loading placeholder when streaming is disabled
|
||||
|
||||
Examples
|
||||
|
||||
Below are a few common examples. You can find the full set of examples in the project README; these are the most useful patterns to re-use in your app.
|
||||
|
||||
Hide the header (simple)
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'javascript',
|
||||
code: 'console.log(1)',
|
||||
raw: 'console.log(1)',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode
|
||||
:node="node"
|
||||
:show-header="false"
|
||||
:loading="false"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
Replace the default header using the `#header-left` and `#header-right` named slots
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'html',
|
||||
code: '<div>Hello</div>',
|
||||
raw: '<div>Hello</div>',
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode
|
||||
:node="node"
|
||||
:loading="false"
|
||||
:show-copy-button="false"
|
||||
>
|
||||
<template #header-left>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium">My HTML</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="px-2 py-1 bg-blue-600 text-white rounded">
|
||||
Run
|
||||
</button>
|
||||
<button class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
|
||||
Inspect
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</CodeBlockNode>
|
||||
</template>
|
||||
```
|
||||
|
||||
Custom loading placeholder (shown when `stream` is false and `loading` is true)
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const code = 'print("hello")'
|
||||
const isLoading = true
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'python',
|
||||
code,
|
||||
raw: code,
|
||||
} satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode
|
||||
:node="node"
|
||||
:stream="false"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<template #loading="{ loading, stream }">
|
||||
<div v-if="loading && !stream" class="p-4 text-center">
|
||||
<div class="animate-spin inline-block w-6 h-6 border-2 border-current border-t-transparent rounded-full" />
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
Initializing editor...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</CodeBlockNode>
|
||||
</template>
|
||||
```
|
||||
|
||||
Events and notes
|
||||
|
||||
- The component emits `copy` when the copy button is clicked (the copy text is supplied as the event payload). Use `@copy="onCopy"` to listen for it.
|
||||
- The component emits `previewCode` only when you attach a `@preview-code` listener; payload is `{ node, artifactType, artifactTitle, id }`. (Without a listener, the built-in preview overlay is available for HTML only.)
|
||||
- To hide a specific toolbar button set, use the boolean props `showCollapseButton`, `showCopyButton`, `showExpandButton`, `showPreviewButton`, `showFontSizeButtons`.
|
||||
- Use `showTooltips` to toggle header action tooltips globally for this block.
|
||||
- The `showHeader` prop controls whether the header is rendered at all.
|
||||
|
||||
Try this — replace the header in a small example:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = { type: 'code_block', language: 'js', code: 'console.log("run")', raw: 'console.log("run")' } satisfies CodeBlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" :show-copy-button="false">
|
||||
<template #header-right>
|
||||
<button @click="() => console.log('run')">
|
||||
Run
|
||||
</button>
|
||||
</template>
|
||||
</CodeBlockNode>
|
||||
</template>
|
||||
```
|
||||
|
||||
Troubleshooting
|
||||
|
||||
If you run into style conflicts (for instance, Tailwind reset/utility rules changing the header's layout), see the main Troubleshooting page which covers Tailwind import ordering and quick fixes: `/guide/troubleshooting`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Compared to traditional Markdown renderers
|
||||
|
||||
Traditional Markdown renderers typically convert a finished Markdown string into a static HTML tree. This library is designed for streaming and interactive workflows and therefore provides capabilities you won't find in a classic renderer:
|
||||
|
||||
- Streaming-first rendering: render partial or incrementally-updated Markdown content without re-parsing the whole document each time. This enables live previews for AI outputs or editors that emit tokens progressively.
|
||||
- Streaming-aware code blocks and "code-jump" UX: large code blocks are updated incrementally and the renderer can maintain cursor/selection context and fine-grained edits.
|
||||
- Built-in diff/code-stream components: show diffs as they arrive (line-by-line or token-by-token) with minimal reflow.
|
||||
- Progressive diagrams and editors: Mermaid and Monaco-based previews update progressively.
|
||||
- Flexible code block rendering: pick Monaco for interactive editing or Shiki for display-only highlighting.
|
||||
|
||||
Quick try — render streaming content and static fallback side-by-side:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const streaming = '# Streaming example\n\nThis appears progressively'
|
||||
const staticMd = '# Static example\n\nRendered once'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MarkdownRender :content="streaming" />
|
||||
<MarkdownRender :content="staticMd" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
description: Override built-in markstream-vue components like image, code_block, mermaid, and link with scoped, project-safe custom mappings.
|
||||
---
|
||||
|
||||
# Override Built-in Components
|
||||
|
||||
Most advanced customization starts with `setCustomComponents`, not parser hooks.
|
||||
|
||||
Use this page when you want to:
|
||||
|
||||
- replace built-in renderers such as `image`, `code_block`, `mermaid`, or `link`
|
||||
- keep overrides scoped to one part of the app
|
||||
- understand which override key matches which node type
|
||||
|
||||
If you want trusted custom tags such as `thinking`, go to [Custom Tags & Advanced Components](/guide/custom-components). If props and slots are enough, stay with [Props & Options](/guide/props).
|
||||
|
||||
## 1. Prefer scoped overrides
|
||||
|
||||
Use `custom-id` on the renderer and register overrides under the same id:
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const DocImage: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
image: DocImage,
|
||||
})
|
||||
```
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const markdown = ''
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender custom-id="docs" :content="markdown" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Why this is the default recommendation:
|
||||
|
||||
- it prevents one project area from changing another
|
||||
- it makes testing and cleanup easier
|
||||
- it matches how the renderer merges scoped and global mappings
|
||||
|
||||
The single-argument form `setCustomComponents({ ... })` is still supported, but it is best treated as a legacy global override.
|
||||
|
||||
## 2. The override keys you will actually use
|
||||
|
||||
| Key | What it replaces | Notes |
|
||||
|-----|------------------|-------|
|
||||
| `image` | `ImageNode` | Great first override for lightboxes, captions, or lazy-loading wrappers |
|
||||
| `link` | `LinkNode` | Useful for analytics, router links, or custom tooltip behavior |
|
||||
| `code_block` | Regular fenced code blocks | Used as the generic fallback after exact language keys and the built-in `mermaid` / `d2` / `infographic` routes |
|
||||
| `mermaid` | Mermaid fenced blocks | Preferred over `code_block` when only Mermaid needs a custom renderer |
|
||||
| `d2` | D2 and `d2lang` fenced blocks | Same routing idea as Mermaid |
|
||||
| `infographic` | Infographic fenced blocks | Use when only infographic output changes |
|
||||
| `inline_code` | Inline code spans | Useful for docs typography or special inline tooling |
|
||||
| `heading`, `paragraph`, `list_item`, `blockquote` | Container nodes | Powerful, but you must render children yourself |
|
||||
|
||||
The full list of built-in node types lives in [Components API](/guide/components) and the exported `CustomComponents` type.
|
||||
|
||||
## 3. Start with leaf-like nodes
|
||||
|
||||
Leaf-like nodes are the easiest place to begin because they do not require you to manually re-render child nodes.
|
||||
|
||||
### Example: replace `image`
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { ImageNodeProps } from 'markstream-vue'
|
||||
|
||||
const props = defineProps<ImageNodeProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure class="docs-image">
|
||||
<img :src="props.node.src" :alt="props.node.alt" loading="lazy">
|
||||
<figcaption v-if="props.node.title || props.node.alt">
|
||||
{{ props.node.title || props.node.alt }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const DocImage: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
image: DocImage,
|
||||
})
|
||||
```
|
||||
|
||||
### Example: switch regular code blocks to `MarkdownCodeBlockNode`
|
||||
|
||||
```ts twoslash
|
||||
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
|
||||
setCustomComponents('docs', {
|
||||
code_block: MarkdownCodeBlockNode,
|
||||
})
|
||||
```
|
||||
|
||||
This only changes regular fenced code blocks. Mermaid, D2, and infographic blocks still route to their specialized renderers unless you override `mermaid`, `d2`, or `infographic`.
|
||||
|
||||
### Example: target one fenced language directly
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const EChartsBlockNode: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
echarts: EChartsBlockNode,
|
||||
})
|
||||
```
|
||||
|
||||
This only catches fences whose language is `echarts`. The routing order for code blocks is:
|
||||
|
||||
- exact language key such as `echarts`
|
||||
- built-in specialized routes such as `mermaid`, `d2`, and `infographic`
|
||||
- generic `code_block`
|
||||
|
||||
### Example: override Mermaid only
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const CustomMermaidBlock: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
mermaid: CustomMermaidBlock,
|
||||
})
|
||||
```
|
||||
|
||||
This is the safest pattern when you want custom export controls, branding, or diagram chrome without affecting all code blocks.
|
||||
|
||||
## 4. What props your override receives
|
||||
|
||||
Every custom renderer receives the basics:
|
||||
|
||||
- `node`
|
||||
- `loading`
|
||||
- `indexKey`
|
||||
- `customId`
|
||||
- `isDark`
|
||||
|
||||
The renderer also forwards feature-specific props:
|
||||
|
||||
- regular code block overrides receive `stream`, `theme`, `darkTheme`, `lightTheme`, `monacoOptions`, `themes`, `minWidth`, `maxWidth`, and anything passed through `codeBlockProps` (`theme` is the recommended field; `darkTheme` / `lightTheme` remain for compatibility)
|
||||
- Mermaid, D2, and infographic overrides receive the corresponding `mermaidProps`, `d2Props`, or `infographicProps`
|
||||
- link and list overrides inherit tooltip and typewriter-related bindings from the parent renderer
|
||||
|
||||
The exported prop types in `markstream-vue` are the easiest way to keep your overrides strongly typed.
|
||||
|
||||
## 5. Container nodes need extra care
|
||||
|
||||
Overriding `heading`, `paragraph`, `list_item`, or `blockquote` is absolutely supported, but those nodes carry children. Your component needs to decide how to render them.
|
||||
|
||||
Practical advice:
|
||||
|
||||
- start with `image`, `link`, `code_block`, `mermaid`, or `inline_code` unless you truly need container-node control
|
||||
- if you override a container node, inspect the built-in renderer in `src/components/*` and mirror the child-rendering pattern you need
|
||||
- keep the override scoped with `custom-id` so you can iterate safely
|
||||
|
||||
## 6. Cleanup for dynamic scopes
|
||||
|
||||
If the override only lives for a temporary view, remove it when that scope is no longer needed:
|
||||
|
||||
```ts twoslash
|
||||
import { removeCustomComponents } from 'markstream-vue'
|
||||
|
||||
removeCustomComponents('docs')
|
||||
```
|
||||
|
||||
This is especially helpful in SPAs, Storybook stories, playgrounds, and test suites.
|
||||
@@ -0,0 +1,319 @@
|
||||
---
|
||||
description: Look up MarkdownRender, CodeBlockNode, MermaidBlockNode, ImageNode, LinkNode, and other exported markstream-vue renderer components.
|
||||
---
|
||||
|
||||
# Renderer & Node Components
|
||||
|
||||
Use this page when you already know you need a specific exported component and want the shortest path to its props, events, CSS requirements, and the page that goes deeper.
|
||||
|
||||
If you are still deciding where to customize the pipeline, start with:
|
||||
|
||||
- Need parser helpers, AST hooks, or `setCustomComponents()` strategy: [API Reference](/guide/api)
|
||||
- Need a scoped renderer replacement: [Override Built-in Components](/guide/component-overrides)
|
||||
- Need trusted custom tags such as `thinking`: [Custom Tags & Advanced Components](/guide/custom-components)
|
||||
- Need configuration, performance, or toolbar tuning: [Props & Options](/guide/props)
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Component | Best for | Key props/events | Extra CSS / peers | Troubleshooting hooks |
|
||||
| --------- | -------- | ---------------- | ----------------- | --------------------- |
|
||||
| `MarkdownRender` | Rendering full AST trees (default export) | Props: `content` / `nodes`, `custom-id`, `final`, `parse-options`, `custom-html-tags`, `is-dark`, `code-block-props`, `mermaid-props`, `d2-props`, `infographic-props`; events: `copy`, `handleArtifactClick`, `click`, `mouseover`, `mouseout` | Import `markstream-vue/index.css` inside a reset-aware layer (CSS is scoped under an internal `.markstream-vue` container) | Use `setCustomComponents(customId, mapping)` + `custom-id` to scope overrides; see [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here) |
|
||||
| `CodeBlockNode` | Monaco-powered code blocks, streaming diffs | `node`, `monacoOptions`, `stream`, `loading`; events: `copy`, `previewCode`; slots `header-left` / `header-right`; diff hover actions live under `monacoOptions` (`diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, `onDiffHunkAction`) | Install `stream-monaco` (peer) + bundle Monaco workers | SSR sends a `<pre><code>` fallback first; blank editor => check worker bundling + client enhancement setup |
|
||||
| `MarkdownCodeBlockNode` | Lightweight highlighting via `shiki` | `node`, `stream`, `loading`; slots `header-left` / `header-right` | Requires `stream-markdown` | Use for SSR-friendly or low-bundle scenarios |
|
||||
| `MermaidBlockNode` | Progressive Mermaid diagrams | `node`, `isDark`, `isStrict`, `maxHeight`, `estimatedPreviewHeightPx`; emits `copy`, `export`, `openModal`, `toggleMode` | Peer `mermaid` >= 11; no extra CSS required | SSR sends readable fallback markup first; for async errors see `/guide/mermaid` |
|
||||
| `D2BlockNode` | Progressive D2 diagrams | `node`, `isDark`, `maxHeight`, `progressiveRender`, `progressiveIntervalMs`; toolbar toggles | Peer `@terrastruct/d2`; no extra CSS | SSR sends fallback/source first; missing peer stays on fallback; see `/guide/d2` |
|
||||
| `MathBlockNode` / `MathInlineNode` | KaTeX rendering | `node` | Install `katex` and import `katex/dist/katex.min.css` | SSR can emit KaTeX HTML when you register a sync loader; otherwise it falls back to raw text |
|
||||
| `ImageNode` | Custom previews/lightboxes | Props: `fallback-src`, `show-caption`, `lazy`, `svg-min-height`, `use-placeholder`; emits `click`, `load`, `error` | None, but respects global CSS | Wrap in a custom component + `setCustomComponents` to intercept events |
|
||||
| `LinkNode` | Animated underline, tooltips | `color`, `underlineHeight`, `showTooltip` | No extra CSS | Browser defaults can override `a` styles; import reset |
|
||||
| `VmrContainerNode` | Custom `:::` containers | `node` (`name`, `attrs`, `loading`, `children`) | Minimal base CSS; override via `setCustomComponents` | JSON attrs are normalized onto `node.attrs` (keys without `data-`); invalid/partial JSON becomes `attrs.attrs`; args after name stored in `attrs.args` |
|
||||
|
||||
## TypeScript exports
|
||||
|
||||
`markstream-vue` exports renderer and component prop interfaces:
|
||||
|
||||
```ts twoslash
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||
type MarkdownRenderCodeBlockProps = NonNullable<MarkdownRenderProps['codeBlockProps']>
|
||||
|
||||
declare const markdownRenderProps: MarkdownRenderProps
|
||||
declare const markdownRenderCodeBlockProps: MarkdownRenderCodeBlockProps
|
||||
declare const codeBlockProps: CodeBlockNodeProps
|
||||
|
||||
// Hover the property names after each dot.
|
||||
markdownRenderProps.content
|
||||
markdownRenderProps.customId
|
||||
markdownRenderProps.isDark
|
||||
markdownRenderProps.codeBlockProps?.showHeader
|
||||
markdownRenderProps.codeBlockProps?.showTooltips
|
||||
markdownRenderProps.codeBlockMonacoOptions
|
||||
markdownRenderProps.codeBlockMonacoOptions?.theme
|
||||
markdownRenderProps.codeBlockMonacoOptions?.languages
|
||||
markdownRenderProps.codeBlockMonacoOptions?.diffHunkActionsOnHover
|
||||
markdownRenderProps.themes
|
||||
|
||||
markdownRenderCodeBlockProps.showFontSizeButtons
|
||||
markdownRenderCodeBlockProps.showCollapseButton
|
||||
|
||||
codeBlockProps.monacoOptions
|
||||
codeBlockProps.monacoOptions?.MAX_HEIGHT
|
||||
codeBlockProps.theme
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `InstanceType<typeof MarkdownRender>['$props']` is the most direct way to inspect the exported component props.
|
||||
- `NodeRendererProps` is the named export for the same public prop surface.
|
||||
- `codeBlockProps` now follows the public `CodeBlockNode` prop surface except for `node`, so hover/completion works for flags like `showHeader`, `showFontSizeButtons`, and `showTooltips`.
|
||||
- Prefer `codeBlockProps.theme` for new code. `darkTheme` / `lightTheme` still exist for backward compatibility.
|
||||
- Hover the property names after each dot in the snippet above, not the imported type names.
|
||||
- If you specifically want the best component-prop hover targets, use the `MarkdownRender` snippet below first.
|
||||
- Only `ts twoslash` and `vue twoslash` fences in this docs site enable hoverable type details.
|
||||
|
||||
Language icons default to the built-in `material` theme. Advanced integrations can inspect or switch icon themes with the exported helpers:
|
||||
|
||||
```ts
|
||||
import { getRegisteredThemes, registerIconTheme, setIconTheme } from 'markstream-vue'
|
||||
|
||||
console.log(getRegisteredThemes()) // ['material']
|
||||
setIconTheme('material')
|
||||
|
||||
// registerIconTheme(...) lets you add your own icon pack before switching.
|
||||
```
|
||||
|
||||
## Pick the right component quickly
|
||||
|
||||
- Use `MarkdownRender` for almost every app-level integration.
|
||||
- Use `CodeBlockNode`, `MermaidBlockNode`, `D2BlockNode`, or `MathBlockNode` when you are composing lower-level node renderers yourself.
|
||||
- Use `ImageNode`, `LinkNode`, or `VmrContainerNode` when you only need to replace one built-in node with custom behavior.
|
||||
- If you render node components directly, wrap them in `<div class="markstream-vue">...</div>` so packaged CSS variables and component styles apply.
|
||||
|
||||
## MarkdownRender
|
||||
|
||||
> Main entry point that takes Markdown AST content (string or parsed structure) and renders with built-in node components.
|
||||
|
||||
### Quick reference
|
||||
|
||||
- **Best for**: full markdown documents in Vite, Nuxt, VitePress.
|
||||
- **Key props**: `content` / `nodes`, `custom-id`, `final`, `parse-options`, `custom-html-tags`
|
||||
- **CSS order**: import reset first, then add `markstream-vue/index.css` inside `@layer components`.
|
||||
|
||||
### CSS scope
|
||||
|
||||
`markstream-vue` scopes its packaged CSS under an internal `.markstream-vue` container to reduce global style conflicts.
|
||||
|
||||
- When you use `MarkdownRender`, this container is already present.
|
||||
- When you render node components directly, wrap them with `<div class="markstream-vue">...</div>` so theme variables and component styles apply.
|
||||
|
||||
### Best hover targets
|
||||
|
||||
Start by hovering `:content`, `custom-id`, `:is-dark`, and `:code-block-monaco-options` in this example:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||
|
||||
const content: MarkdownRenderProps['content'] = '# Hello'
|
||||
const customId: MarkdownRenderProps['customId'] = 'docs'
|
||||
const isDark: MarkdownRenderProps['isDark'] = true
|
||||
const monacoOptions: MarkdownRenderProps['codeBlockMonacoOptions'] = {
|
||||
theme: 'vitesse-dark',
|
||||
languages: ['typescript', 'vue'],
|
||||
MAX_HEIGHT: 520,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="content"
|
||||
:custom-id="customId"
|
||||
:is-dark="isDark"
|
||||
:code-block-monaco-options="monacoOptions"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Usage ladder
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockMonacoOptions } from 'markstream-vue'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Hello\n\nUse custom-id to scope styles.'
|
||||
const monacoOptions = {
|
||||
theme: 'vitesse-dark',
|
||||
themes: ['vitesse-dark', 'vitesse-light'],
|
||||
languages: ['typescript', 'vue'],
|
||||
MAX_HEIGHT: 520,
|
||||
diffHunkActionsOnHover: true,
|
||||
} satisfies CodeBlockMonacoOptions
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:content="md"
|
||||
:code-block-monaco-options="monacoOptions"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts
|
||||
// Register custom nodes
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
import CustomImageNode from './CustomImageNode.vue'
|
||||
|
||||
setCustomComponents('docs', {
|
||||
image: CustomImageNode,
|
||||
})
|
||||
```
|
||||
|
||||
```css
|
||||
/* styles/main.css */
|
||||
@import 'modern-css-reset';
|
||||
@tailwind base;
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
|
||||
[data-custom-id='docs'] .prose {
|
||||
max-width: 720px;
|
||||
}
|
||||
```
|
||||
|
||||
### Performance props
|
||||
|
||||
- **Batch rendering**: `batchRendering`, `initialRenderBatchSize`, `renderBatchSize`, `renderBatchDelay`, and `renderBatchBudgetMs` control how many nodes switch from skeletons to real components per frame. This only activates when virtualization is disabled (`:max-live-nodes="0"`).
|
||||
- **Deferred heavy nodes**: `deferNodesUntilVisible` and `viewportPriority` are enabled by default so Mermaid, D2, Monaco, and KaTeX only load near the viewport.
|
||||
- **Virtualization window**: `maxLiveNodes` limits how many rendered nodes stay in the DOM, and `liveNodeBuffer` controls overscan. See [Performance](/guide/performance).
|
||||
- **Code block fallback**: use `renderCodeBlocksAsPre` and `codeBlockStream` to downgrade standard code fences to `<pre><code>` or disable streaming updates during heavy workloads.
|
||||
|
||||
### Common issues
|
||||
|
||||
- **Broken styles**: start with the [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here).
|
||||
- **Utility class leakage**: pass `custom-id` and scope overrides with `[data-custom-id="docs"]`.
|
||||
- **SSR errors**: the renderer itself is SSR-safe; only gate your own browser-only page logic or manual peer initialization with client-only / mounted guards.
|
||||
|
||||
### Use this when
|
||||
|
||||
- You want the default parser + renderer path with the fewest moving parts.
|
||||
- You need one integration point for streaming, virtualization, batching, custom tags, and scoped overrides.
|
||||
- You want to choose between `content` and `nodes` without wiring individual node renderers yourself.
|
||||
|
||||
### Most common combinations
|
||||
|
||||
- `content` + `custom-id`: static or lightly customized documents.
|
||||
- `nodes` + `final`: streaming or server-preparsed workflows.
|
||||
- `custom-html-tags` + `setCustomComponents(customId, mapping)`: trusted custom tags such as `thinking`.
|
||||
- `renderCodeBlocksAsPre`: downgrade heavy code blocks during SSR or constrained environments.
|
||||
|
||||
## CodeBlockNode
|
||||
|
||||
> Monaco-backed code block renderer with streaming diff support and interactive toolbar actions.
|
||||
|
||||
- **Best for**: rich code review, diff inspection, live patches, hover actions.
|
||||
- **Key props**: `node`, `monacoOptions`, `stream`, `loading`
|
||||
- **Events**: `copy`, `previewCode`
|
||||
- **Slots**: `header-left`, `header-right`
|
||||
- **Peers**: `stream-monaco`, plus Monaco worker setup in your bundler
|
||||
- **Common gotcha**: if the editor area is blank, verify worker bundling and SSR guards first
|
||||
|
||||
Reach for this when the code block itself is part of the product experience. If you only need syntax highlighting, prefer `MarkdownCodeBlockNode`.
|
||||
|
||||
Deep dive: [CodeBlockNode](/guide/code-block-node), [Monaco](/guide/monaco)
|
||||
|
||||
## MarkdownCodeBlockNode
|
||||
|
||||
> Lightweight syntax-highlighted code block renderer powered by Shiki via `stream-markdown`.
|
||||
|
||||
- **Best for**: SSR-friendly docs, blog-style pages, smaller bundles
|
||||
- **Key props**: `node`, `stream`, `loading`
|
||||
- **Slots**: `header-left`, `header-right`
|
||||
- **Peers**: `stream-markdown`
|
||||
- **Common gotcha**: if highlighting never appears, confirm `stream-markdown` is installed and loaded in the environment where rendering happens
|
||||
|
||||
Choose this when you do not need Monaco's editing surface or diff interactions.
|
||||
|
||||
## MermaidBlockNode
|
||||
|
||||
> Progressive Mermaid renderer with copy/export/modal hooks.
|
||||
|
||||
- **Best for**: large Mermaid graphs, AI-generated diagrams, user-triggered export
|
||||
- **Key props**: `node`, `isDark`, `isStrict`, `maxHeight`, `estimatedPreviewHeightPx`
|
||||
- **Events**: `copy`, `export`, `openModal`, `toggleMode`
|
||||
- **Peer**: `mermaid` >= 11
|
||||
- **Common gotcha**: treat Mermaid as client enhancement; SSR already returns fallback markup, but preview rendering still initializes on the client
|
||||
|
||||
`MarkdownRender` estimates `estimatedPreviewHeightPx` for Mermaid fences when callers do not provide it. Pass it manually only when rendering `MermaidBlockNode` directly or when a custom `mermaid` renderer already knows the first-preview height.
|
||||
|
||||
Deep dive: [Mermaid](/guide/mermaid), [MermaidBlockNode](/guide/mermaid-block-node)
|
||||
|
||||
## D2BlockNode
|
||||
|
||||
> Progressive D2 diagram renderer for structured architecture diagrams and sequence-like layouts.
|
||||
|
||||
- **Best for**: D2-driven docs, generated architecture views, progressive rendering
|
||||
- **Key props**: `node`, `isDark`, `maxHeight`, `progressiveRender`, `progressiveIntervalMs`
|
||||
- **Peer**: `@terrastruct/d2`
|
||||
- **Common gotcha**: missing peer falls back to source display instead of the rendered diagram
|
||||
|
||||
Deep dive: [D2](/guide/d2)
|
||||
|
||||
## MathBlockNode / MathInlineNode
|
||||
|
||||
> KaTeX-backed block and inline math renderers.
|
||||
|
||||
- **Best for**: technical docs, formulas, AI math responses
|
||||
- **Key prop**: `node`
|
||||
- **Peer**: `katex`
|
||||
- **Required CSS**: `katex/dist/katex.min.css`
|
||||
- **Common gotcha**: invisible math usually means KaTeX CSS is missing, not that parsing failed
|
||||
|
||||
## ImageNode
|
||||
|
||||
> Built-in image renderer with sensible defaults for captioning, fallbacks, and lazy behavior.
|
||||
|
||||
- **Best for**: custom previews, analytics hooks, lightbox integration
|
||||
- **Key props**: `fallback-src`, `show-caption`, `lazy`, `svg-min-height`, `use-placeholder`
|
||||
- **Events**: `click`, `load`, `error`
|
||||
- **Common pattern**: wrap this with a custom component and register it through `setCustomComponents(customId, { image: CustomImageNode })`
|
||||
|
||||
Deep dive: [ImageNode](/guide/image-node)
|
||||
|
||||
## LinkNode
|
||||
|
||||
> Link renderer with underline animation and optional tooltip behavior.
|
||||
|
||||
- **Best for**: docs sites and chat surfaces where link affordance needs to match your design system
|
||||
- **Key props**: `color`, `underlineHeight`, `showTooltip`
|
||||
- **Common gotcha**: browser defaults or resets can override anchor styles, so verify CSS order before assuming the component is broken
|
||||
|
||||
## VmrContainerNode
|
||||
|
||||
> Renderer for `:::` containers and other normalized block containers coming from the parser.
|
||||
|
||||
- **Best for**: callouts, notices, AI-specific block types, container-style custom components
|
||||
- **Key prop**: `node` with `name`, `attrs`, `loading`, and `children`
|
||||
- **Normalization detail**: JSON attrs are normalized onto `node.attrs`; invalid or partial JSON is preserved under `attrs.attrs`; args after the container name are stored in `attrs.args`
|
||||
- **Common pattern**: use this together with `custom-html-tags` or parser hooks when you need trusted structured blocks
|
||||
|
||||
## Direct node rendering checklist
|
||||
|
||||
When you bypass `MarkdownRender` and mount node components directly:
|
||||
|
||||
- Wrap them in a `.markstream-vue` container.
|
||||
- Import the same CSS you would for the full renderer.
|
||||
- Install and guard only the peers needed by the nodes you actually render.
|
||||
- Scope any replacement styles with a parent selector or `data-custom-id` equivalent.
|
||||
|
||||
## Need a different layer of the stack?
|
||||
|
||||
- Use [API Reference](/guide/api) for parser helpers, `setCustomComponents`, and AST hooks.
|
||||
- Use [Props & Options](/guide/props) for full renderer prop behavior.
|
||||
- Use [Troubleshooting](/guide/troubleshooting) if the right component is already chosen but CSS, peers, or SSR still misbehave.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for wanting to contribute!
|
||||
|
||||
Development commands to know:
|
||||
|
||||
- `pnpm dev` — start the library playground
|
||||
- `pnpm play` — start the playground (same as dev)
|
||||
- `pnpm build` — build the library
|
||||
- `pnpm build:analyze` — build with bundle visualizer reports
|
||||
- `pnpm size:check` — enforce package size budgets
|
||||
- `pnpm test` — run unit tests
|
||||
- `pnpm docs:dev` — start the VitePress docs server
|
||||
|
||||
Size budget env overrides (optional):
|
||||
|
||||
- `MAX_DIST_BYTES`
|
||||
- `MAX_JS_CHUNK_BYTES`
|
||||
- `MAX_PACK_TGZ_BYTES`
|
||||
- `MAX_PACK_UNPACKED_BYTES`
|
||||
|
||||
How to add a docs page:
|
||||
|
||||
1. Add a markdown file under `docs/` or `docs/guide`.
|
||||
2. Update `docs/.vitepress/config.ts` sidebar if needed.
|
||||
3. Run `pnpm docs:dev` to preview the page locally and ensure everything builds.
|
||||
4. Open a PR with your changes.
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Keep docs concise and example-driven
|
||||
- If adding code snippets to docs, verify they run in `playground` or tests
|
||||
- Add tests for any new features in `test/` and update `README` / docs accordingly
|
||||
|
||||
Quick try — run the docs site locally and add a simple page to verify the flow:
|
||||
|
||||
```bash
|
||||
pnpm docs:dev
|
||||
# open http://localhost:5173, add docs/guide/new-page.md and confirm build
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
description: Add trusted custom tags such as thinking and map them to advanced markstream-vue components without rewriting the parser.
|
||||
---
|
||||
|
||||
# Custom Tags & Advanced Components
|
||||
|
||||
Use this page when Markdown needs to contain trusted, component-like tags such as `thinking`, `answer-box`, or other domain-specific blocks.
|
||||
|
||||
The recommended path is:
|
||||
|
||||
1. register the tag with `custom-html-tags`
|
||||
2. map the resulting node type with `setCustomComponents`
|
||||
3. keep the mapping scoped with `custom-id`
|
||||
|
||||
Use tag-like names in `custom-html-tags`, for example `thinking`, `answer-box`, or `my_component`.
|
||||
Namespaced forms like `foo:bar` are ignored. Built-in override keys such as `code_block` stay reserved for node renderer overrides, so trusted custom tags should still be declared explicitly via `custom-html-tags`.
|
||||
|
||||
Reach for parser hooks only after this flow stops being enough.
|
||||
|
||||
If these tags live inside a docs site or VitePress theme, pair this page with [Docs Site & VitePress](/guide/vitepress-docs-integration) so the theme-level registration and CSS order stay in one place.
|
||||
|
||||
## 1. The simplest custom-tag setup
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const ThinkingNode: Component
|
||||
|
||||
setCustomComponents('chat', {
|
||||
thinking: ThinkingNode,
|
||||
})
|
||||
```
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const markdown = '<thinking>Step by step</thinking>'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="chat"
|
||||
:custom-html-tags="['thinking']"
|
||||
:content="markdown"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
Once the tag is allowlisted, the parser emits a custom node whose `type` is the tag name itself.
|
||||
|
||||
## 2. A practical Vue component for nested content
|
||||
|
||||
Custom tags usually contain Markdown inside them. The easiest way to preserve that nested Markdown is to render the tag body with another `MarkdownRender`.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
node: {
|
||||
type: 'thinking'
|
||||
content?: string
|
||||
loading?: boolean
|
||||
}
|
||||
customId?: string
|
||||
isDark?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="thinking-box" :data-loading="props.node.loading || undefined">
|
||||
<header class="thinking-box__title">
|
||||
Thinking
|
||||
</header>
|
||||
|
||||
<MarkdownRender
|
||||
:content="String(props.node.content ?? '')"
|
||||
:custom-id="props.customId"
|
||||
:is-dark="props.isDark"
|
||||
:custom-html-tags="['thinking']"
|
||||
:typewriter="false"
|
||||
:viewport-priority="false"
|
||||
:defer-nodes-until-visible="false"
|
||||
:max-live-nodes="0"
|
||||
:batch-rendering="false"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
```
|
||||
|
||||
That nested-renderer pattern is also what keeps repeated and nested custom tags predictable.
|
||||
|
||||
## 3. What the parser gives you
|
||||
|
||||
For a trusted custom tag, the emitted node typically includes:
|
||||
|
||||
- `type`: the tag name, for example `thinking`
|
||||
- `tag`: the original tag name
|
||||
- `content`: the inner Markdown or text content
|
||||
- `attrs`: extracted tag attributes when available
|
||||
- `loading`: whether the tag is still in a streaming mid-state
|
||||
- `autoClosed`: whether the parser temporarily auto-closed the tag during streaming
|
||||
|
||||
The exact `attrs` shape can vary, so treat it as raw attribute data that your component normalizes for its own needs.
|
||||
|
||||
## 4. Repeated and nested custom tags
|
||||
|
||||
This flow is designed to support:
|
||||
|
||||
- repeated tags in the same document
|
||||
- nested custom tags
|
||||
- streaming mid-states while the closing tag has not arrived yet
|
||||
|
||||
Tips that help in practice:
|
||||
|
||||
- pass the same `custom-html-tags` list into nested renderers
|
||||
- disable batching and viewport deferral inside small nested shells when you want the most predictable streaming behavior
|
||||
- keep the outer renderer scoped with `custom-id`
|
||||
|
||||
## 5. When `custom-html-tags` is enough, and when it is not
|
||||
|
||||
Use `custom-html-tags` plus `setCustomComponents` when:
|
||||
|
||||
- the syntax is already tag-like
|
||||
- you trust the source
|
||||
- you mainly need a different renderer, not a different grammar
|
||||
|
||||
Move to [Advanced Parser Hooks](/guide/advanced) when:
|
||||
|
||||
- tags need token rewriting before they become stable nodes
|
||||
- you must merge, split, or reshape nodes after parsing
|
||||
- the source format is not well represented by a tag-like wrapper
|
||||
|
||||
## 6. Scope and cleanup still matter
|
||||
|
||||
Even for custom tags, prefer scoped mappings:
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { removeCustomComponents, setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const ThinkingNode: Component
|
||||
|
||||
setCustomComponents('chat', { thinking: ThinkingNode })
|
||||
|
||||
// later, when the scoped renderer is no longer needed
|
||||
removeCustomComponents('chat')
|
||||
```
|
||||
|
||||
This keeps custom behavior local to the page, route, or app surface that actually needs it.
|
||||
@@ -0,0 +1,110 @@
|
||||
# D2 quick start
|
||||
|
||||
D2 diagrams render progressively in markstream-vue: as soon as the syntax becomes valid, a preview appears and keeps refining as more tokens stream in. If rendering fails, the last successful preview stays visible and the source is preserved.
|
||||
|
||||
## 1. Install & import
|
||||
|
||||
```bash
|
||||
pnpm add @terrastruct/d2
|
||||
```
|
||||
|
||||
No extra CSS import is required. Keep `markstream-vue/index.css` after your reset and inside `@layer components` if you use Tailwind/UnoCSS.
|
||||
|
||||
```css
|
||||
@import 'modern-css-reset';
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Streaming example
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const content = ref('')
|
||||
const steps = [
|
||||
'```d2\n',
|
||||
'direction: right\n',
|
||||
'Client -> API: request\n',
|
||||
'API -> DB: query\n',
|
||||
'DB -> API: rows\n',
|
||||
'API -> Client: response\n',
|
||||
'```\n',
|
||||
]
|
||||
|
||||
let i = 0
|
||||
const id = setInterval(() => {
|
||||
content.value += steps[i] || ''
|
||||
i++
|
||||
if (i >= steps.length)
|
||||
clearInterval(id)
|
||||
}, 120)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="content" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Quick try — paste this Markdown into a page or component:
|
||||
|
||||
```md
|
||||
\`\`\`d2
|
||||
direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query
|
||||
DB -> API: rows
|
||||
API -> Client: response
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
## 3. Advanced component: `D2BlockNode`
|
||||
|
||||
Use `D2BlockNode` if you want toolbar controls (copy/export/mode toggle) or to override themes and progressive timing.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { D2BlockNodeProps } from 'markstream-vue'
|
||||
import { D2BlockNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: 'direction: right\nClient -> API: request\nAPI -> DB: query',
|
||||
raw: 'direction: right\nClient -> API: request\nAPI -> DB: query',
|
||||
} satisfies D2BlockNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<D2BlockNode :node="node" :progressive-interval-ms="500" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Props to know:
|
||||
- `progressiveRender`, `progressiveIntervalMs` control incremental re-rendering while streaming.
|
||||
- `themeId`, `darkThemeId` let you pin a D2 theme.
|
||||
- `showHeader`, `showModeToggle`, `showCopyButton`, `showExportButton`, `showCollapseButton` control the toolbar.
|
||||
|
||||
## 4. Dark mode
|
||||
|
||||
Pass `:is-dark="true"` (or add a `.dark` ancestor via `MarkdownRender`) to render the diagram in dark mode. markstream-vue prefers the app theme over OS preference to keep previews stable.
|
||||
|
||||
## 5. Custom loaders (optional)
|
||||
|
||||
If you disabled the loader earlier, or want to load from a CDN/global build:
|
||||
|
||||
```ts twoslash
|
||||
import { enableD2, setD2Loader } from 'markstream-vue'
|
||||
|
||||
// Re-enable the default loader
|
||||
enableD2()
|
||||
|
||||
// Or supply your own loader
|
||||
setD2Loader(() => (window as any).D2)
|
||||
```
|
||||
|
||||
If `@terrastruct/d2` is missing, the renderer falls back to showing the source text until it becomes available.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Docs assets & screenshots
|
||||
|
||||
If you're adding screenshots or GIFs to the documentation, please follow these guidelines:
|
||||
|
||||
- Keep file size small (use SVG where possible or optimized PNG/JPG/GIF)
|
||||
- Store assets in `docs/.vitepress/public/` so they are served as static files
|
||||
- Use descriptive filenames and include a caption under the image
|
||||
- Prefer SVG for diagrams and small screenshots
|
||||
|
||||
Example usage in a markdown page:
|
||||
|
||||

|
||||
|
||||
Thanks for making docs better! Please link the original playground page if you add a screenshot for reproducibility.
|
||||
|
||||
Quick try — add an SVG screenshot (example):
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
@@ -0,0 +1,588 @@
|
||||
# ECharts integration
|
||||
|
||||
`markstream-vue` supports rendering ECharts charts through custom components. This page covers three recommended approaches, from simplest to most advanced.
|
||||
|
||||
## Why ECharts with markstream-vue?
|
||||
|
||||
ECharts is a powerful charting library that works naturally with the streaming architecture of `markstream-vue`. You can:
|
||||
|
||||
- Write chart options directly in Markdown code blocks
|
||||
- Stream chart updates progressively (ideal for AI-generated content)
|
||||
- Reuse existing ECharts themes and configurations
|
||||
- Export charts as images
|
||||
|
||||
## Approach 1: Custom CodeBlock component (Recommended)
|
||||
|
||||
This approach treats ECharts as a special code block language, similar to how Mermaid is handled. It's the most user-friendly and follows the existing architecture pattern.
|
||||
|
||||
### 1.1 Create the ECharts component
|
||||
|
||||
Create `EChartsBlockNode.vue`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
node: {
|
||||
type: 'code_block'
|
||||
language: string
|
||||
code: string
|
||||
loading?: boolean
|
||||
}
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Only render echarts code blocks
|
||||
const isECharts = computed(() => props.node.language === 'echarts' || props.node.language === 'chart')
|
||||
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
async function initChart() {
|
||||
if (!isECharts.value || !chartRef.value)
|
||||
return
|
||||
|
||||
// Dispose existing instance
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
// Initialize with theme based on dark mode
|
||||
const theme = props.isDark ? 'dark' : undefined
|
||||
chartInstance = echarts.init(chartRef.value, theme)
|
||||
|
||||
try {
|
||||
// Parse JSON option from code
|
||||
const option = JSON.parse(props.node.code)
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[ECharts] Invalid option:', error)
|
||||
chartInstance?.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// Reinitialize on theme change
|
||||
watch(() => props.isDark, () => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
// Reinitialize on code change
|
||||
watch(() => props.node.code, () => {
|
||||
if (chartInstance) {
|
||||
try {
|
||||
const option = JSON.parse(props.node.code)
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[ECharts] Invalid option:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(initChart)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
chartInstance?.dispose()
|
||||
chartInstance = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isECharts" ref="chartRef" class="echarts-container" style="width: 100%; height: 400px" />
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.echarts-container {
|
||||
margin: 1rem 0;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 1.2 Register the custom component
|
||||
|
||||
```ts
|
||||
// main.ts or your component entry
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
import EChartsBlockNode from './components/EChartsBlockNode.vue'
|
||||
|
||||
setCustomComponents({
|
||||
code_block: EChartsBlockNode
|
||||
})
|
||||
```
|
||||
|
||||
### 1.3 Use in Markdown
|
||||
|
||||
Now you can write ECharts charts directly in Markdown:
|
||||
|
||||
```markdown
|
||||
\```echarts
|
||||
{
|
||||
"title": { "text": "Sales Data" },
|
||||
"tooltip": {},
|
||||
"xAxis": {
|
||||
"type": "category",
|
||||
"data": ["Mon", "Tue", "Wed", "Thu", "Fri"]
|
||||
},
|
||||
"yAxis": { "type": "value" },
|
||||
"series": [{
|
||||
"type": "bar",
|
||||
"data": [120, 200, 150, 80, 70]
|
||||
}]
|
||||
}
|
||||
\```
|
||||
```
|
||||
|
||||
### 1.4 Install ECharts
|
||||
|
||||
```bash
|
||||
pnpm add echarts
|
||||
# or
|
||||
npm install echarts
|
||||
```
|
||||
|
||||
### Pros & Cons
|
||||
|
||||
**Pros:**
|
||||
- Familiar code block syntax
|
||||
- Follows existing Mermaid pattern
|
||||
- Works with streaming updates
|
||||
- Easy to implement
|
||||
|
||||
**Cons:**
|
||||
- Overrides all `code_block` rendering (need to handle non-echarts blocks)
|
||||
- Requires JSON parsing (no JS expressions)
|
||||
|
||||
---
|
||||
|
||||
## Approach 2: Custom HTML tag
|
||||
|
||||
Use a custom HTML-like tag for ECharts charts, providing clearer separation from regular code blocks.
|
||||
|
||||
### 2.1 Configure custom HTML tags
|
||||
|
||||
```ts
|
||||
// parser configuration (or pass `custom-html-tags` to MarkdownRender)
|
||||
import { getMarkdown } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown('echarts', {
|
||||
customHtmlTags: ['echarts', 'chart'],
|
||||
})
|
||||
```
|
||||
|
||||
### 2.2 Register the component
|
||||
|
||||
```ts
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
import EChartsComponent from './components/EChartsComponent.vue'
|
||||
|
||||
setCustomComponents({
|
||||
echarts: EChartsComponent,
|
||||
chart: EChartsComponent
|
||||
})
|
||||
```
|
||||
|
||||
### 2.3 Create the component
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
node: {
|
||||
type: string
|
||||
tag: string
|
||||
content: string
|
||||
attrs?: Record<string, string>
|
||||
}
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// Parse attributes for width/height
|
||||
const width = computed(() => props.node.attrs?.width || '100%')
|
||||
const height = computed(() => props.node.attrs?.height || '400px')
|
||||
|
||||
async function initChart() {
|
||||
if (!chartRef.value)
|
||||
return
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
const theme = props.isDark ? 'dark' : undefined
|
||||
chartInstance = echarts.init(chartRef.value, theme)
|
||||
|
||||
try {
|
||||
const option = JSON.parse(props.node.content)
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[ECharts] Invalid option:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.isDark, initChart)
|
||||
watch(() => props.node.content, () => {
|
||||
if (chartInstance) {
|
||||
try {
|
||||
const option = JSON.parse(props.node.content)
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[ECharts] Invalid option:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(initChart)
|
||||
onBeforeUnmount(() => {
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chartRef" class="echarts-chart" :style="{ width, height }" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2.4 Use in Markdown
|
||||
|
||||
```markdown
|
||||
<echarts width="100%" height="500px">
|
||||
{
|
||||
"series": [{
|
||||
"type": "pie",
|
||||
"data": [
|
||||
{ "value": 335, "name": "Direct" },
|
||||
{ "value": 310, "name": "Email" },
|
||||
{ "value": 234, "name": "Affiliate" }
|
||||
]
|
||||
}]
|
||||
}
|
||||
</echarts>
|
||||
```
|
||||
|
||||
### Pros & Cons
|
||||
|
||||
**Pros:**
|
||||
- Clear type separation
|
||||
- Supports custom attributes (width, height)
|
||||
- Doesn't interfere with code blocks
|
||||
|
||||
**Cons:**
|
||||
- Requires parser configuration
|
||||
- Less familiar syntax
|
||||
- Not standard Markdown
|
||||
|
||||
---
|
||||
|
||||
## Approach 3: VmrContainer integration
|
||||
|
||||
Use ECharts inside a custom container (similar to admonitions), ideal for documentation with chart descriptions.
|
||||
|
||||
### 3.1 Register container component
|
||||
|
||||
```ts
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
import EChartsContainerNode from './components/EChartsContainerNode.vue'
|
||||
|
||||
setCustomComponents({
|
||||
vmr_container: EChartsContainerNode
|
||||
})
|
||||
```
|
||||
|
||||
### 3.2 Create the container component
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
node: {
|
||||
type: 'vmr_container'
|
||||
name: string
|
||||
children?: Array<{ type: string, raw: string }>
|
||||
}
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Only handle echarts containers
|
||||
const isEChartsContainer = computed(() => props.node.name === 'echarts')
|
||||
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// Extract JSON from children
|
||||
const chartOption = computed(() => {
|
||||
if (!props.node.children || props.node.children.length === 0) {
|
||||
return null
|
||||
}
|
||||
const code = props.node.children[0].raw
|
||||
try {
|
||||
return JSON.parse(code)
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
async function initChart() {
|
||||
if (!isEChartsContainer.value || !chartRef.value || !chartOption.value)
|
||||
return
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
const theme = props.isDark ? 'dark' : undefined
|
||||
chartInstance = echarts.init(chartRef.value, theme)
|
||||
chartInstance.setOption(chartOption.value, true)
|
||||
}
|
||||
|
||||
watch(() => props.isDark, initChart)
|
||||
watch(chartOption, (option) => {
|
||||
if (chartInstance && option) {
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(initChart)
|
||||
onBeforeUnmount(() => {
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isEChartsContainer" class="vmr-container vmr-container-echarts">
|
||||
<div ref="chartRef" style="width: 100%; height: 400px" />
|
||||
<slot v-if="!chartOption" />
|
||||
</div>
|
||||
<div v-else class="vmr-container" :class="`vmr-container-${node.name}`">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vmr-container-echarts {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.dark .vmr-container-echarts {
|
||||
border-color: #374151;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 3.3 Use in Markdown
|
||||
|
||||
```markdown
|
||||
:::echarts
|
||||
{
|
||||
"title": { "text": "Traffic Sources" },
|
||||
"series": [{
|
||||
"type": "pie",
|
||||
"radius": "50%",
|
||||
"data": [
|
||||
{ "value": 1048, "name": "Search" },
|
||||
{ "value": 735, "name": "Direct" },
|
||||
{ "value": 580, "name": "Marketing" }
|
||||
]
|
||||
}]
|
||||
}
|
||||
:::
|
||||
```
|
||||
|
||||
### Pros & Cons
|
||||
|
||||
**Pros:**
|
||||
- Consistent with existing container system
|
||||
- Supports additional content around charts
|
||||
- Good for documentation
|
||||
|
||||
**Cons:**
|
||||
- More complex setup
|
||||
- Requires parsing children
|
||||
|
||||
---
|
||||
|
||||
## SSR Considerations
|
||||
|
||||
ECharts requires the DOM and doesn't support server-side rendering. Use one of these strategies:
|
||||
|
||||
### For Nuxt 3
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
```
|
||||
|
||||
### For SSR with fallback
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const isClient = ref(false)
|
||||
const markdown = '# ECharts demo'
|
||||
|
||||
onMounted(() => {
|
||||
isClient.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender v-if="isClient" :content="markdown" />
|
||||
<div v-else class="echarts-placeholder">
|
||||
Loading chart...
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Check for window in component
|
||||
|
||||
```ts
|
||||
import { computed } from 'vue'
|
||||
|
||||
const canRender = computed(() => typeof window !== 'undefined' && isECharts.value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### ResizeObserver for responsive charts
|
||||
|
||||
```ts
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
|
||||
useResizeObserver(chartRef, (entries) => {
|
||||
if (chartInstance && entries[0]) {
|
||||
const { width, height } = entries[0].contentRect
|
||||
if (width > 0 && height > 0) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Export as image
|
||||
|
||||
```ts
|
||||
function exportChart() {
|
||||
if (!chartInstance)
|
||||
return
|
||||
const url = chartInstance.getDataURL({
|
||||
type: 'png',
|
||||
pixelRatio: 2,
|
||||
backgroundColor: props.isDark ? '#1a1a1a' : '#fff'
|
||||
})
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'chart.png'
|
||||
link.click()
|
||||
}
|
||||
```
|
||||
|
||||
### Theme switching with ECharts themes
|
||||
|
||||
```ts
|
||||
import dark from 'echarts/theme/dark'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const theme = computed(() => props.isDark ? 'dark' : undefined)
|
||||
chartInstance = echarts.init(chartRef.value, theme.value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Which approach should I choose?
|
||||
|
||||
| Approach | Best for | Complexity |
|
||||
|----------|----------|------------|
|
||||
| **CodeBlock** | Most users, simple integration | ⭐ |
|
||||
| **Custom HTML tag** | Advanced users, multiple chart types | ⭐⭐ |
|
||||
| **VmrContainer** | Documentation, styled containers | ⭐⭐⭐ |
|
||||
|
||||
**Recommendation:** Start with **Approach 1 (CodeBlock)**. It's the most straightforward and aligns with how users already interact with Mermaid diagrams.
|
||||
|
||||
---
|
||||
|
||||
## Complete example
|
||||
|
||||
Here's a complete minimal setup using Approach 1:
|
||||
|
||||
```ts
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue'
|
||||
// main.ts
|
||||
import { createApp } from 'vue'
|
||||
import EChartsBlockNode from './EChartsBlockNode.vue'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'echarts'
|
||||
|
||||
// Register custom component
|
||||
setCustomComponents({
|
||||
code_block: EChartsBlockNode
|
||||
})
|
||||
|
||||
const app = createApp({
|
||||
components: { MarkdownRender },
|
||||
template: `
|
||||
<MarkdownRender :content="markdown" />
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
markdown: `<!-- eslint-disable -->
|
||||
\`\`\`echarts
|
||||
{
|
||||
"xAxis": { "type": "category", "data": ["A", "B", "C"] },
|
||||
"yAxis": { "type": "value" },
|
||||
"series": [{ "type": "bar", "data": [10, 20, 30] }]
|
||||
}
|
||||
\`\`\``
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Chart not rendering** — Check browser console for JSON parse errors. Ensure the code block content is valid JSON.
|
||||
|
||||
2. **Blank container** — Verify ECharts is installed and the DOM element has dimensions. Set explicit width/height styles.
|
||||
|
||||
3. **Theme not switching** — ECharts instances need to be re-created when changing themes. Use `watch(() => props.isDark)` to dispose and reinitialize.
|
||||
|
||||
4. **SSR errors** — Wrap the component in `<ClientOnly>` (Nuxt) or check `typeof window !== 'undefined'` before initializing.
|
||||
|
||||
5. **Memory leaks** — Always call `chartInstance.dispose()` in `onBeforeUnmount`.
|
||||
|
||||
Still having issues? Check the [ECharts documentation](https://echarts.apache.org/en/index.html) or open an issue on GitHub.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Examples
|
||||
|
||||
Short examples; see `playground` for interactive demos.
|
||||
|
||||
## Streaming Markdown
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const content = ref('')
|
||||
const fullContent = `# Streaming Content\n\nThis text appears progressively...`
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i < fullContent.length) {
|
||||
content.value += fullContent[i]
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 50)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="content" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Typewriter + TypeScript example
|
||||
- Use `typewriter` prop to enable enter transition
|
||||
|
||||
## Rendering with `nodes`
|
||||
- Call `parseMarkdownToStructure` from `stream-markdown-parser` and pass `nodes` to `MarkdownRender` for custom rendering.
|
||||
|
||||
Try this — quickly stream a Markdown string with `typewriter` enabled:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Streaming test\n\nThis text appears with a subtle enter animation.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" :typewriter="true" />
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Features
|
||||
|
||||
- Progressive Mermaid: diagrams render incrementally
|
||||
- Progressive D2: diagrams render incrementally with source fallback
|
||||
- Streaming-first rendering: handle tokenized and partial markdown
|
||||
- Monaco streaming integration: efficient updates for large code blocks
|
||||
- Streaming diff code blocks: show diffs as they are generated
|
||||
- Flexible code rendering: Monaco or Shiki
|
||||
- Pluggable parse hooks: pre- and post-transform tokens
|
||||
- Full Markdown support (tables, math, optional Emoji, task checkboxes, code blocks). Emoji is provided as an optional plugin and is not enabled by default — enable it via `getMarkdown` options or the component `customMarkdownIt` prop.
|
||||
- Lubu features: math rendering via KaTeX, i18n support
|
||||
|
||||
Quick try — render a Markdown string with streaming-friendly features:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Streamed Features\n\n```mermaid\ngraph LR\nA-->B\n```'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
# ImageNode — Custom preview handling
|
||||
|
||||
`ImageNode` renders images and emits events for user interactions. We expose a `click` event on images so you can take over the click behavior (for example to open a custom image preview / lightbox) without replacing the whole renderer.
|
||||
|
||||
- Events: `click` — emitted when a user clicks a successfully loaded image. Payload is `[Event, string]` where the second item is the src actually used for rendering (this may be a fallback URL).
|
||||
- Events: `load` / `error` — emitted when an image finishes loading or fails, payload is the image `src`.
|
||||
|
||||
## Example: wrapper + VitePress registration
|
||||
|
||||
Create a small wrapper component that intercepts `click` and use `setCustomComponents` to register it in the client app.
|
||||
|
||||
### 1) `CustomImageNode.vue` (simple wrapper)
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import { ImageNode } from 'markstream-vue'
|
||||
|
||||
const emit = defineEmits(['load', 'error', 'click'])
|
||||
|
||||
function onImageClick(payload: [Event, string]) {
|
||||
const [, src] = payload
|
||||
// Simple example: open the image in a new tab for preview.
|
||||
// Replace this with a modal/lightbox call in real apps.
|
||||
window.open(src, '_blank')
|
||||
// Re-emit the click so parent code can also react if needed.
|
||||
emit('click', payload)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ImageNode v-bind="$attrs" @load="emit('load', $event)" @error="emit('error', $event)" @click="onImageClick" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2) Register in VitePress theme enhance
|
||||
|
||||
```ts twoslash
|
||||
import type { App, Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
// docs/.vitepress/theme/index.ts
|
||||
declare const CustomImageNode: Component
|
||||
|
||||
export default ({ app }: { app: App }) => {
|
||||
setCustomComponents('vitepress-image-preview', { image: CustomImageNode })
|
||||
}
|
||||
```
|
||||
|
||||
## Optional: richer preview interactions
|
||||
|
||||
For zoom/gesture/slide support, call a lightbox library such as `photoswipe`, `fslightbox` or `basiclightbox` inside `onImageClick`, or control an application-level modal to show the preview.
|
||||
|
||||
## Summary
|
||||
|
||||
The `click` event from `ImageNode` is the primary hook for implementing custom previews. The usual approach is wrapping the original component and registering it with `setCustomComponents` in the client app. The wrapper can also forward `load`/`error` events or add additional behavior (analytics, alternative lazy-load handling, custom placeholders, etc.).
|
||||
|
||||
Quick try — register `CustomImageNode` in a VitePress client enhance and test clicking images in the `playground`:
|
||||
|
||||
```ts twoslash
|
||||
// docs/.vitepress/theme/index.ts
|
||||
import type { App, Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const CustomImageNode: Component
|
||||
|
||||
export default ({ app }: { app: App }) => setCustomComponents('vitepress-image-preview', { image: CustomImageNode })
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description: Task-first guide for choosing the right markstream-vue path across installation, streaming usage, VitePress docs sites, component overrides, and migration.
|
||||
---
|
||||
|
||||
# Guide
|
||||
|
||||
This guide is organized by task first, framework second. Start with the shortest path that matches what you are trying to do today.
|
||||
|
||||
## I Want To Get Something Working Quickly
|
||||
|
||||
- [Install by scenario](/guide/installation) for peer-dependency decisions and CSS order.
|
||||
- [Quick Start](/guide/quick-start) for the smallest Vue 3 example.
|
||||
- [Usage & Streaming](/guide/usage) if you need to choose between `content` and `nodes`.
|
||||
- [Docs Site & VitePress](/guide/vitepress-docs-integration) if you are wiring stable Markdown pages, theme CSS, and custom tags inside a docs site.
|
||||
- [AI Chat & Streaming](/guide/ai-chat-streaming) if the UI updates continuously and you need one guided path for peers, parsing, performance, and troubleshooting.
|
||||
- [Troubleshooting by Symptom](/guide/troubleshooting-path) if you do not yet know whether the issue is CSS, peers, SSR, or custom-tag wiring.
|
||||
- [Props & Options](/guide/props) if the renderer already works and you only need configuration.
|
||||
|
||||
## I Want To Customize Rendering
|
||||
|
||||
- [API Reference](/guide/api) for parser helpers, scoping, and render-pipeline entry points.
|
||||
- [Renderer & Node Components](/guide/components) for exported renderer and node component reference.
|
||||
- [Override Built-in Components](/guide/component-overrides) to replace `image`, `code_block`, `mermaid`, `link`, or other built-ins.
|
||||
- [Custom Tags & Advanced Components](/guide/custom-components) to support trusted tags such as `thinking`.
|
||||
- [Advanced Parser Hooks](/guide/advanced) and [Parser API](/guide/parser-api) for token or AST-level customization.
|
||||
|
||||
## I Want To Adopt It In An Existing App
|
||||
|
||||
- [Nuxt SSR](/nuxt-ssr) for browser-only peers and client-only guards.
|
||||
- [AI / Skills workflows](/guide/ai-workflows) for copyable prompts, reusable checklists, and migration tasks.
|
||||
- [Migrate from react-markdown](/guide/react-markdown-migration) and the [Migration Cookbook](/guide/react-markdown-migration-cookbook) for React teams.
|
||||
- [Troubleshooting](/guide/troubleshooting) when the install works but styles, peers, or SSR do not.
|
||||
|
||||
## Framework Entry Points
|
||||
|
||||
### Vue 3 (markstream-vue) ⭐ Recommended
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| [Installation](/guide/installation) | Install the package and only the peers you actually need |
|
||||
| [Quick Start](/guide/quick-start) | Render your first Markdown document |
|
||||
| [Usage & Streaming](/guide/usage) | Decide between `content` and `nodes` |
|
||||
| [Docs Site & VitePress](/guide/vitepress-docs-integration) | Guided path for docs pages, `enhanceApp`, trusted tags, and CSS order |
|
||||
| [AI Chat & Streaming](/guide/ai-chat-streaming) | Guided path for chat UIs, SSE, and token-by-token output |
|
||||
| [API Reference](/guide/api) | Parser helpers, scoping, and render-pipeline entry points |
|
||||
| [Renderer & Node Components](/guide/components) | Renderer and node component reference |
|
||||
| [Customization](/guide/component-overrides) | Override built-ins and add custom tags |
|
||||
|
||||
### Vue 2 (markstream-vue2)
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| [Installation](/guide/vue2-installation) | Vue 2 specific setup |
|
||||
| [Quick Start](/guide/vue2-quick-start) | Vue 2 examples |
|
||||
| [Components & API](/guide/vue2-components) | Vue 2 component reference |
|
||||
|
||||
### React (markstream-react)
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| [Installation](/guide/react-installation) | React specific setup |
|
||||
| [Quick Start](/guide/react-quick-start) | React examples |
|
||||
| [React Components](/guide/react-components) | React renderer and node components |
|
||||
| [Migrate from react-markdown](/guide/react-markdown-migration) | Migration path for existing React Markdown apps |
|
||||
| [Migration Cookbook](/guide/react-markdown-migration-cookbook) | Before/after recipes for common migration scenarios |
|
||||
|
||||
### Angular (markstream-angular)
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| [Installation](/guide/angular-installation) | Angular specific setup |
|
||||
| [Quick Start](/guide/angular-quick-start) | Standalone Angular examples |
|
||||
|
||||
### Nuxt
|
||||
|
||||
- [Nuxt SSR Guide](/nuxt-ssr) for client-only boundaries, workers, and browser-only peers.
|
||||
@@ -0,0 +1,35 @@
|
||||
# AntV Infographic
|
||||
|
||||
`markstream-vue` supports rendering [AntV Infographic](https://infographic.antv.vision/) charts.
|
||||
|
||||
## 1. Installation
|
||||
|
||||
This feature depends on the `@antv/infographic` library.
|
||||
|
||||
```bash
|
||||
npm install @antv/infographic
|
||||
```
|
||||
|
||||
## 2. Example
|
||||
|
||||
Use the `infographic` code block in Markdown to render charts:
|
||||
|
||||
````md
|
||||
```infographic
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
items
|
||||
- label Step 1
|
||||
desc Start
|
||||
- label Step 2
|
||||
desc Processing
|
||||
- label Step 3
|
||||
desc Complete
|
||||
```
|
||||
````
|
||||
|
||||

|
||||
|
||||
## 3. Resources
|
||||
|
||||
- [AntV Infographic Official Website](https://infographic.antv.vision/) - Explore more chart templates and syntax details.
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
description: Install markstream-vue with the right peer dependency set for docs sites, AI chat UIs, Mermaid, KaTeX, Monaco, and large documents.
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
If you only want the renderer on screen quickly, install the main package first and add peers only for the features you actually use.
|
||||
|
||||
## 1. Minimal install
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue
|
||||
# or
|
||||
npm install markstream-vue
|
||||
# or
|
||||
yarn add markstream-vue
|
||||
```
|
||||
|
||||
Then continue with [Quick Start](/guide/quick-start) if you only need basic Markdown rendering.
|
||||
|
||||
## 2. Choose peers by capability
|
||||
|
||||
| Capability | Packages | When you need it |
|
||||
|------------|----------|------------------|
|
||||
| Lightweight highlighted code blocks | `stream-markdown` | Docs sites, SSR, lower bundle budgets |
|
||||
| Monaco-powered code blocks | `stream-monaco` | Copy/preview/expand controls and richer code UX |
|
||||
| Mermaid diagrams | `mermaid` | Fenced `mermaid` blocks |
|
||||
| D2 diagrams | `@terrastruct/d2` | Fenced `d2` or `d2lang` blocks |
|
||||
| KaTeX math | `katex` | Inline or block math rendering |
|
||||
|
||||
## 3. Common install recipes
|
||||
|
||||
### Docs site or SSR-first app
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue stream-markdown
|
||||
```
|
||||
|
||||
Then continue with [Docs Site & VitePress](/guide/vitepress-docs-integration) if you are wiring a docs site, content hub, or VitePress theme.
|
||||
|
||||
### AI / chat UI with richer code blocks and diagrams
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue stream-monaco mermaid katex
|
||||
```
|
||||
|
||||
Then follow [AI Chat & Streaming](/guide/ai-chat-streaming) for the recommended `nodes` + `final` data flow and chat-specific tuning.
|
||||
|
||||
### Diagram-heavy content
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue mermaid @terrastruct/d2 katex
|
||||
```
|
||||
|
||||
### Everything enabled
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue stream-markdown stream-monaco mermaid @terrastruct/d2 katex
|
||||
```
|
||||
|
||||
## 4. CSS order matters as much as installation
|
||||
|
||||
The package entry already imports the default stylesheet, but when your app uses reset layers or utility frameworks you usually want explicit control over order.
|
||||
|
||||
```css
|
||||
@import 'modern-css-reset';
|
||||
@tailwind base;
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
Also import KaTeX CSS when you use math:
|
||||
|
||||
```ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
`stream-monaco`, `mermaid`, and `@terrastruct/d2` do not need extra CSS imports from this package.
|
||||
|
||||
## 5. Optional loaders (only for CDN or custom control)
|
||||
|
||||
After installing peers, default loaders are already enabled. Only call loader helpers if you previously disabled them or want a custom loader, for example with CDN assets:
|
||||
|
||||
```ts twoslash
|
||||
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue'
|
||||
|
||||
enableMermaid()
|
||||
enableKatex()
|
||||
enableD2()
|
||||
```
|
||||
|
||||
## 6. First-run checklist
|
||||
|
||||
- If you render standalone node components, wrap them in `<div class="markstream-vue">...</div>`.
|
||||
- If math does not render, check that `katex` is installed and its CSS is imported.
|
||||
- If Monaco is blank, verify worker bundling and browser-only guards.
|
||||
- If styles look wrong, check [Troubleshooting](/guide/troubleshooting#css-looks-wrong-start-here).
|
||||
|
||||
## 7. Quick test
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||
|
||||
const md: MarkdownRenderProps['content'] = '# Hello from markstream-vue!'
|
||||
const customId: MarkdownRenderProps['customId'] = 'install-check'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
:custom-id="customId"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
Next steps:
|
||||
|
||||
- [Quick Start](/guide/quick-start) for the smallest integration
|
||||
- [Usage & Streaming](/guide/usage) for `content` vs `nodes`
|
||||
- [AI Chat & Streaming](/guide/ai-chat-streaming) for chat UIs, SSE, and token streams
|
||||
- [Override Built-in Components](/guide/component-overrides) if you need custom rendering
|
||||
@@ -0,0 +1,87 @@
|
||||
# Legacy build & compatibility guide (Vite / Webpack)
|
||||
|
||||
Purpose
|
||||
- This document explains how to use legacy builds to improve compatibility with older browsers (for example older iOS / Safari) and when you should instead fix issues in source code.
|
||||
|
||||
Key reminder
|
||||
- Some problems (notably RegExp engine-level syntax like lookbehind `(?<!...)`) cannot be fixed by polyfills. Legacy builds transform syntax and inject API polyfills but cannot add RegExp engine features to an older JS runtime. If you encounter such issues, you must either rewrite the code or delay construction/runtime-detect and fallback. See `ios-regex-compat.md` for details.
|
||||
|
||||
What this page covers
|
||||
- Vite: `@vitejs/plugin-legacy` usage and caveats
|
||||
- Webpack: common two-build (modern + legacy) approaches
|
||||
- Managing polyfills (`core-js`, `regenerator-runtime`) and targets
|
||||
- Testing, CI and quick debug checklist
|
||||
|
||||
Vite (recommended if your project uses Vite)
|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
pnpm add -D @vitejs/plugin-legacy
|
||||
```
|
||||
|
||||
Example `vite.config.ts`:
|
||||
|
||||
```ts
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
legacy({
|
||||
targets: ['defaults', 'iOS >= 11'],
|
||||
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
|
||||
renderLegacyChunks: true,
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Notes
|
||||
- `targets` controls which transforms/polyfills are applied. Set to your minimum supported browsers.
|
||||
- `additionalLegacyPolyfills` injects runtime libraries into the legacy bundle.
|
||||
- `renderLegacyChunks` enables differential chunks served via `type="module"` / `nomodule`.
|
||||
- Important: plugin-legacy cannot rewrite engine-level RegExp syntax (lookbehind). Keep incompatible regex out of literal module scope or rewrite them.
|
||||
|
||||
Webpack
|
||||
|
||||
Webpack does not provide a single plugin equivalent to Vite's plugin-legacy. Common approach: build two bundles with Babel (modern + legacy) and serve them with `type="module"` / `nomodule`.
|
||||
|
||||
Key deps:
|
||||
|
||||
```bash
|
||||
pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli
|
||||
```
|
||||
|
||||
Example approach
|
||||
- Create a `babel.config.js` that supports `BABEL_ENV=legacy` to enable `useBuiltIns: 'usage'` and a legacy `targets` set. Run two webpack builds: one modern, one legacy. Serve modern bundle with `<script type="module">` and legacy with `<script nomodule>`.
|
||||
|
||||
Polyfills
|
||||
- Use `core-js` for standard API polyfills (v3). For generators/async: `regenerator-runtime/runtime`.
|
||||
- Prefer differential serving so modern bundles stay small.
|
||||
|
||||
RegExp and engine-level syntax
|
||||
- RegExp features such as lookbehind are implemented by the JS engine. Polyfills cannot add them. Either
|
||||
- rewrite the regex to a compatible form, or
|
||||
- delay regex construction (use `new RegExp(...)`) and use runtime feature-detection + fallback.
|
||||
|
||||
Testing & CI
|
||||
- Add `pnpm run build` (modern + legacy) in CI and run smoke tests on older browsers (BrowserStack / SauceLabs or Xcode simulator).
|
||||
- Add a static check to block new lookbehind/unicode-regex features (see examples below).
|
||||
|
||||
Quick local checks
|
||||
|
||||
```bash
|
||||
# Static scan for advanced regex in source
|
||||
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
|
||||
```
|
||||
|
||||
Summary
|
||||
- Legacy builds are useful for many syntax & API gaps, but they are not a replacement for fixing engine-level RegExp syntax in source. Prefer: source fix -> legacy bundle -> CI + old-device smoke tests.
|
||||
|
||||
Quick test — run a local legacy build (Vite + plugin-legacy):
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# verify modern + legacy bundles output and test on an older device or simulator
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# Math Rendering Options (KaTeX)
|
||||
|
||||
This project uses KaTeX for math rendering when available. KaTeX is a peer dependency and must be installed separately.
|
||||
|
||||
Install KaTeX:
|
||||
|
||||
```bash
|
||||
pnpm add katex
|
||||
```
|
||||
|
||||
Import KaTeX styles in your entry file (e.g., `main.ts`):
|
||||
|
||||
```ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
## CDN usage (no bundler)
|
||||
|
||||
If you load KaTeX via `<script>` tags (so `window.katex` exists), the library can use it. If KaTeX loads after the first render, reset the loader once:
|
||||
|
||||
```ts
|
||||
import { createKaTeXWorkerFromCDN, enableKatex, setKatexLoader, setKaTeXWorker } from 'markstream-vue'
|
||||
|
||||
// use the CDN global (UMD) on the main thread
|
||||
setKatexLoader(() => (window as any).katex)
|
||||
enableKatex(() => (window as any).katex)
|
||||
|
||||
// optional: offload renderToString into a worker (CDN-loaded inside worker)
|
||||
const { worker } = createKaTeXWorkerFromCDN({
|
||||
mode: 'classic',
|
||||
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js',
|
||||
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/mhchem.min.js',
|
||||
})
|
||||
if (worker)
|
||||
setKaTeXWorker(worker)
|
||||
```
|
||||
|
||||
Customize math parsing behaviour with `getMarkdown` options or `setDefaultMathOptions`:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, setDefaultMathOptions } from 'markstream-vue'
|
||||
|
||||
setDefaultMathOptions({
|
||||
commands: ['infty', 'perp'],
|
||||
escapeExclamation: true,
|
||||
})
|
||||
|
||||
const md = getMarkdown()
|
||||
```
|
||||
|
||||
If KaTeX is not installed, the renderer will leave math content as text and still be import-safe under SSR.
|
||||
|
||||
Important: when writing source Markdown, always use literal (escaped) backslashes
|
||||
for TeX parenthesis delimiters. Write `\\(...\\)` rather than `\(...\)` so
|
||||
the parser can reliably detect inline TeX delimiters. Unescaped `\(...\)`
|
||||
cannot be distinguished from ordinary parentheses and may not be parsed as math.
|
||||
|
||||
```ts
|
||||
// Try snippet to demonstrate KaTeX usage
|
||||
const example = `\\(E = mc^2\\)`
|
||||
console.log(example)
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
# Override MermaidBlockNode in MarkdownRender
|
||||
|
||||
If you want to completely replace the `mermaid` renderer used by `MarkdownRender` (for example in the `playground-demo`), you can use `setCustomComponents` and provide a programmatic renderer that mounts `MermaidBlockNode` with custom event handlers.
|
||||
|
||||
Example:
|
||||
|
||||
```ts twoslash
|
||||
import type { MermaidBlockEvent } from 'markstream-vue'
|
||||
import { MermaidBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
import { h } from 'vue'
|
||||
|
||||
declare function uploadSvgToServer(svg: string): void
|
||||
|
||||
setCustomComponents('playground-demo', {
|
||||
mermaid: (props: any) => h(MermaidBlockNode, {
|
||||
...props,
|
||||
onCopy: async (ev: MermaidBlockEvent<{ type: 'copy', text: string }>) => {
|
||||
// Optional: take over the copy behavior (clipboard + toast/analytics)
|
||||
ev.preventDefault()
|
||||
const text = ev.payload?.text ?? ''
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function')
|
||||
await navigator.clipboard.writeText(text)
|
||||
},
|
||||
onExport: (ev: MermaidBlockEvent) => {
|
||||
// read the rendered svg from ev.svgElement and upload or save
|
||||
const svgEl = ev.svgElement as SVGElement | null
|
||||
if (svgEl) {
|
||||
const svgString = new XMLSerializer().serializeToString(svgEl)
|
||||
uploadSvgToServer(svgString)
|
||||
}
|
||||
ev.preventDefault()
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Use `onExport` / `onCopy` (camelCase) when passing listeners via `h()`.
|
||||
- Call `ev.preventDefault()` to prevent the default behavior inside `MermaidBlockNode`.
|
||||
- `ev.svgElement` (if present) gives direct access to the rendered SVG DOM node.
|
||||
- `ev.svgString` (if present) contains a serialized string of the SVG (ready to upload or send to an API).
|
||||
|
||||
Quick try — test the override by mounting the custom renderer inside your app's client entry and interacting with onExport in the playground.
|
||||
|
||||
```ts twoslash
|
||||
// .vitepress/clientAppEnhance.ts or playground client entry
|
||||
import type { MermaidBlockEvent } from 'markstream-vue'
|
||||
import { MermaidBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
import { h } from 'vue'
|
||||
|
||||
setCustomComponents('playground-demo', {
|
||||
mermaid: (props: any) => h(MermaidBlockNode, { ...props, onExport: (ev: MermaidBlockEvent) => {
|
||||
ev.preventDefault()
|
||||
console.log('exported', ev.svgElement)
|
||||
} })
|
||||
})
|
||||
```
|
||||
|
||||
Playground demo: the repo includes a runnable playground page demonstrating intercepting the export event and uploading the svg — open: /playground/src/pages/mermaid-export-demo.vue or run the playground and visit the route for the demo.
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# MermaidBlockNode (Component)
|
||||
|
||||
`MermaidBlockNode` is a lightweight, extensible renderer for Mermaid diagrams intended for embedding interactive Mermaid charts within documents. The component supports progressive rendering, source/preview toggling, copying source, exporting SVG, and a pseudo-fullscreen modal (with zoom and drag).
|
||||
|
||||
## Props (key)
|
||||
- `node: any` — the Mermaid code node (required)
|
||||
- `isDark?: boolean` — dark mode flag
|
||||
- `loading?: boolean` — initial loading placeholder
|
||||
- `maxHeight?: string | null` — maximum height
|
||||
- `estimatedPreviewHeightPx?: number` — first-preview height reserved before Mermaid finishes rendering; `MarkdownRender` fills this automatically for Mermaid fences
|
||||
- `isStrict?: boolean` — run Mermaid in `securityLevel: 'strict'` with DOMPurify + HTML-label hardening; use this when rendering untrusted diagrams to strip inline scripts/`javascript:` URLs that could otherwise leak into the SVG
|
||||
- `onRenderError?: (error: unknown, code: string, container: HTMLElement) => boolean | void` — custom error handler called when mermaid rendering fails. Return `true` to prevent the default error display. Receives the error, the raw mermaid source code, and the container DOM element so you can render custom content.
|
||||
- Header / control props (all optional, default `true`):
|
||||
- `showHeader`, `showModeToggle`, `showCopyButton`, `showExportButton`, `showFullscreenButton`, `showCollapseButton`, `showZoomControls`, `showTooltips`
|
||||
- `enableWheelZoom?: boolean` — enable Ctrl/Cmd + wheel zoom in the canvas (default `false`)
|
||||
- Timeouts (ms): `workerTimeoutMs`, `parseTimeoutMs`, `renderTimeoutMs`, `fullRenderTimeoutMs` (defaults: `1400/1800/2500/4000`)
|
||||
- Streaming tuning: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`
|
||||
|
||||
## Slots
|
||||
- `header-left` — replace the left area (defaults to Mermaid icon + label)
|
||||
- `header-center` — replace the center area (defaults to preview/source toggle)
|
||||
- `header-right` — replace the right-side action buttons (take full control of the default controls)
|
||||
|
||||
## Emits
|
||||
The component emits events using a unified `MermaidBlockEvent` object. Listeners can call `preventDefault()` on the event to stop the component's default behavior:
|
||||
|
||||
- `copy` — copy button clicked, signature: `(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)` (call `ev.preventDefault()` to stop the built-in clipboard write + "Copied" state)
|
||||
- `export` — export button clicked, signature: `(ev: MermaidBlockEvent<{ type: 'export' }>)`
|
||||
- `openModal` — request to open the pseudo-fullscreen modal, signature: `(ev: MermaidBlockEvent<{ type: 'open-modal' }>)`
|
||||
- `toggleMode` — toggle between `source` and `preview`, signature: `(target: 'source' | 'preview', ev: MermaidBlockEvent<{ type: 'toggle-mode'; target: 'source' | 'preview' }>)`
|
||||
|
||||
### Intercept example
|
||||
Completely override the component's default export behavior:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { MermaidBlockEvent } from 'markstream-vue'
|
||||
import { MermaidBlockNode } from 'markstream-vue'
|
||||
|
||||
function onExport(ev: any /* MermaidBlockEvent */) {
|
||||
ev.preventDefault()
|
||||
// The component exposes the rendered SVG element on the event as `svgElement`.
|
||||
const svgEl = ev.svgElement as SVGElement | null
|
||||
if (!svgEl) {
|
||||
console.warn('No svg element available')
|
||||
return
|
||||
}
|
||||
const svgString = new XMLSerializer().serializeToString(svgEl)
|
||||
uploadSvg(svgString)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MermaidBlockNode :node="node" @export="onExport" />
|
||||
</template>
|
||||
```
|
||||
|
||||
> Note: The event object includes both `svgElement` (DOM node) and `svgString` (serialized SVG) on `export` and `openModal` events — use whichever is more convenient.
|
||||
|
||||
## Slot example — fully replace right-side controls
|
||||
|
||||
```vue
|
||||
<MermaidBlockNode :node="node" :showExportButton="false">
|
||||
<template #header-right>
|
||||
<button @click="downloadSvg">Download</button>
|
||||
<button @click="openCustomModal">Open custom modal</button>
|
||||
</template>
|
||||
</MermaidBlockNode>
|
||||
```
|
||||
|
||||
## Recommended usage
|
||||
- To implement custom export/upload behavior, call `preventDefault()` in the `export` listener and extract the SVG from the rendered DOM in your handler.
|
||||
- To fully replace the header UI, use the `header-*` slots and set the corresponding `show*` props to `false` to hide the default controls.
|
||||
- If Mermaid content originates from users/LLMs or any untrusted source, set `:is-strict="true"` so the component sanitizes the SVG and disables HTML labels; this closes the gap where crafted `javascript:` URLs or event handlers could sneak into the rendered output.
|
||||
- In AI chat scenarios, use `onRenderError` to show raw mermaid source instead of the default error message:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { MermaidBlockNode } from 'markstream-vue'
|
||||
|
||||
function handleMermaidError(_err: unknown, code: string, container: HTMLElement) {
|
||||
// Show the raw mermaid text as a code block instead of an error message
|
||||
const pre = document.createElement('pre')
|
||||
pre.className = 'text-sm font-mono whitespace-pre-wrap p-4'
|
||||
pre.textContent = code
|
||||
container.replaceChildren(pre)
|
||||
return true // prevent default error display
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MermaidBlockNode :node="node" :on-render-error="handleMermaidError" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Notes:
|
||||
- `svgString` is already included in `export` and `openModal` event payloads (the serialized SVG string is available on the event), so consumers can upload or post the SVG without re-serializing.
|
||||
- This page is linked from the docs sidebar and a runnable playground demo is provided in the repo (route: `/mermaid-export-demo`, file: `playground/src/pages/mermaid-export-demo.vue`).
|
||||
|
||||
## See also
|
||||
|
||||
- Override `MermaidBlockNode` (use `setCustomComponents` with `MarkdownRender`): [Override MermaidBlockNode in MarkdownRender](./mermaid-block-node-override.md)
|
||||
|
||||
Playground demo: there's a runnable playground page demonstrating how to intercept `export` and upload `ev.svgString` — see the playground route `mermaid-export-demo` (file: `playground/src/pages/mermaid-export-demo.vue`).
|
||||
|
||||
Try this — quick mermaid example you can paste into a component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = `\n\`\`\`mermaid\ngraph TD\nA-->B\n\`\`\`\n`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
# Mermaid export demo
|
||||
|
||||
This repository includes a runnable playground demo that shows how to intercept the export event from `MermaidBlockNode` and upload the serialized SVG (ev.svgString).
|
||||
|
||||
- Playground route: `/mermaid-export-demo`
|
||||
- File: `playground/src/pages/mermaid-export-demo.vue`
|
||||
|
||||
Why use this demo?
|
||||
|
||||
- Demonstrates how to use `setCustomComponents` to override the mermaid renderer inside `MarkdownRender`.
|
||||
- Shows how to intercept `export` and use `ev.svgString` (or `ev.svgElement`) to upload or send the SVG to an API.
|
||||
|
||||
Try it: run the playground and visit the route `/mermaid-export-demo`.
|
||||
|
||||
Try it locally — runnable example
|
||||
|
||||
You can copy the following single-file component into your app or into the playground to try the export handler immediately. It demonstrates a minimal `setCustomComponents` override that intercepts the `export` event and posts the serialized SVG string to a (fake) API.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender, { MermaidBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
import { h } from 'vue'
|
||||
|
||||
// minimal uploader helper (replace with real upload)
|
||||
async function uploadSvgToServer(svgString: string) {
|
||||
// Example: POST to an API endpoint
|
||||
// await fetch('/api/upload', { method: 'POST', body: svgString })
|
||||
return `https://example.com/uploads/mermaid-${Date.now()}.svg`
|
||||
}
|
||||
|
||||
setCustomComponents('my-demo', {
|
||||
mermaid: (props: any) => h(MermaidBlockNode, {
|
||||
...props,
|
||||
// intercept export events (prevent default and upload svgString)
|
||||
onExport: async (ev: any) => {
|
||||
ev.preventDefault()
|
||||
const svg = ev.svgString ?? (ev.svgElement ? new XMLSerializer().serializeToString(ev.svgElement) : null)
|
||||
if (!svg) {
|
||||
console.warn('no svg available on export')
|
||||
return
|
||||
}
|
||||
const url = await uploadSvgToServer(svg)
|
||||
// Show uploaded URL or persist as needed
|
||||
console.log('uploaded svg to', url)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const md = `
|
||||
\`\`\`mermaid
|
||||
graph LR
|
||||
A[User] --> B[Server]
|
||||
B --> C[Storage]
|
||||
\`\`\`
|
||||
`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender custom-id="my-demo" :content="md" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Run this in the repo playground with:
|
||||
|
||||
```bash
|
||||
pnpm play
|
||||
# open http://localhost:5173/mermaid-export-demo or navigate to the route in the playground
|
||||
```
|
||||
|
||||
Live demo (embedded)
|
||||
|
||||
You can try the demo right inside the docs site — if the demo is hosted at the public playground URL this iframe will load it (or run the playground locally and open the route):
|
||||
|
||||
<div style="max-width:960px; margin: 1rem 0; position:relative;">
|
||||
<iframe
|
||||
src="https://markstream-vue.simonhe.me/mermaid-export-demo"
|
||||
title="Mermaid export demo"
|
||||
loading="lazy"
|
||||
sandbox="allow-forms allow-scripts allow-popups allow-same-origin"
|
||||
style="width:100%; height:540px; border:1px solid #e5e7eb; border-radius:8px; background:#0b0b0b"
|
||||
></iframe>
|
||||
|
||||
<!-- Helpful fallback when embedding is blocked (X-Frame-Options or CSP) -->
|
||||
<div style="margin-top:0.5rem; font-size:0.9rem; color:var(--vp-c-default);">
|
||||
If the demo shows a blank or black iframe, the remote site may disallow embedding (X-Frame-Options/CSP) or the demo may be offline.
|
||||
<strong>Try one of these:</strong>
|
||||
<ul>
|
||||
<li><a href="https://markstream-vue.simonhe.me/mermaid-export-demo" target="_blank" rel="noopener noreferrer">Open the demo in a new tab</a></li>
|
||||
<li>Run the playground locally with <code>pnpm play</code> and visit <code>/mermaid-export-demo</code></li>
|
||||
<li>Import the playground into CodeSandbox using the link below to edit and run the demo inline</li>
|
||||
</ul>
|
||||
|
||||
<!-- Visual fallback: a representative screenshot stored in docs public assets -->
|
||||
<div style="margin-top:0.75rem; border:1px solid rgba(0,0,0,0.06); border-radius:6px; overflow:hidden; max-width:680px;">
|
||||
<img src="/screenshots/mermaid-demo.svg" alt="Mermaid export demo screenshot" style="width:100%; display:block; background:#fff" />
|
||||
<div style="padding:0.5rem 0.75rem; font-size:0.85rem; color:var(--vp-c-muted);">Preview: Mermaid export demo (run locally if the iframe is blocked)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Open in CodeSandbox
|
||||
|
||||
If you'd like to edit this demo in a web-based environment, import the repo into CodeSandbox (GitHub import) and open the `playground` folder. Example URL (edit as needed):
|
||||
|
||||
[Open the playground in CodeSandbox (GitHub import)](https://codesandbox.io/s/github/Simon-He95/markstream-vue/tree/main/playground)
|
||||
|
||||
CodeSandbox (live editable)
|
||||
|
||||
<div style="max-width:960px; margin: 1rem 0;">
|
||||
<iframe
|
||||
src="https://codesandbox.io/embed/github/Simon-He95/markstream-vue/tree/main/playground?autoresize=1&fontsize=14&hidenavigation=1"
|
||||
title="Playground (CodeSandbox)"
|
||||
style="width:100%; height:520px; border:1px solid #e5e7eb; border-radius:8px;"
|
||||
sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
|
||||
<div style="margin-top:0.5rem; font-size:0.9rem; color:var(--vp-c-muted);">
|
||||
Edit and run the playground in CodeSandbox — open in a new tab if you prefer the full editor experience:
|
||||
<a href="https://codesandbox.io/s/github/Simon-He95/markstream-vue/tree/main/playground" target="_blank" rel="noopener noreferrer">Open playground in CodeSandbox</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
# Mermaid quick start
|
||||
|
||||
Mermaid diagrams stream progressively in `markstream-vue`: as soon as the syntax becomes valid the chart renders, then refines as more tokens arrive. This page covers setup, a streaming example, and common fixes.
|
||||
|
||||
## 1. Install & import
|
||||
|
||||
```bash
|
||||
pnpm add mermaid
|
||||
```
|
||||
|
||||
No extra Mermaid CSS import is required. Keep `markstream-vue/index.css` after your reset and inside `@layer components` when using Tailwind/UnoCSS so utility layers do not override the renderer styles.
|
||||
|
||||
```css
|
||||
@import 'modern-css-reset';
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Streaming example
|
||||
|
||||
Mermaid renders as soon as the snippet is syntactically valid. The snippet below shows a gradual update (ideal for AI responses or long-running tasks):
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const content = ref('')
|
||||
const steps = [
|
||||
'```mermaid\n',
|
||||
'graph TD\n',
|
||||
'A[Start]-->B{Is valid?}\n',
|
||||
'B -- Yes --> C[Render]\n',
|
||||
'B -- No --> D[Wait]\n',
|
||||
'```\n',
|
||||
]
|
||||
|
||||
let i = 0
|
||||
const id = setInterval(() => {
|
||||
content.value += steps[i] || ''
|
||||
i++
|
||||
if (i >= steps.length)
|
||||
clearInterval(id)
|
||||
}, 120)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="content" />
|
||||
<!-- Diagram progressively appears as content streams in -->
|
||||
</template>
|
||||
```
|
||||
|
||||
Quick try — paste this Markdown into a page or component:
|
||||
|
||||
```md
|
||||
\`\`\`mermaid
|
||||
graph LR
|
||||
A[Start]-->B
|
||||
B-->C[End]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 3. Advanced component: `MermaidBlockNode`
|
||||
|
||||
Need header controls, export buttons, or a pseudo-fullscreen modal? Use [`MermaidBlockNode`](/guide/mermaid-block-node) or override the default renderer via [setCustomComponents](/guide/mermaid-block-node-override). A runnable playground demo lives at `/mermaid-export-demo`.
|
||||
If diagrams come from untrusted users/LLMs, pass `:is-strict="true"` to enable Mermaid's strict mode and SVG sanitization so injected HTML/event handlers are stripped before render.
|
||||
|
||||
## 4. Troubleshooting checklist
|
||||
|
||||
1. **Peer not installed** — run `pnpm add mermaid`. Without it the renderer falls back to showing source text.
|
||||
2. **Async errors** — check the browser console for Mermaid logs. Versions prior to 11 are unsupported; upgrade to ≥ 11.
|
||||
3. **SSR guard** — Mermaid needs the DOM. Wrap the component in `<ClientOnly>` for Nuxt or check `typeof window !== 'undefined'` before mounting in SSR contexts.
|
||||
4. **Heavy graphs** — consider pre-rendering server-side (mermaid CLI) or caching SVG output; the component exposes `svgString` when using `MermaidBlockNode` export events.
|
||||
|
||||
Still stuck? Reproduce the issue in the playground (`pnpm play`) with a minimal Markdown sample and link it when opening a bug report.
|
||||
|
||||
## CDN usage (no bundler)
|
||||
|
||||
If you load Mermaid via CDN and want progressive off-main-thread parsing, inject a CDN-backed worker:
|
||||
|
||||
```ts twoslash
|
||||
import { createMermaidWorkerFromCDN, enableMermaid, setMermaidLoader, setMermaidWorker } from 'markstream-vue'
|
||||
|
||||
// use the CDN global (UMD) on the main thread
|
||||
setMermaidLoader(() => (window as any).mermaid)
|
||||
enableMermaid(() => (window as any).mermaid)
|
||||
|
||||
// optional: worker used for parse/prefix checks during streaming
|
||||
const { worker } = createMermaidWorkerFromCDN({
|
||||
mode: 'module',
|
||||
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs',
|
||||
})
|
||||
if (worker)
|
||||
setMermaidWorker(worker)
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Monaco Internals
|
||||
|
||||
This page covers build and runtime tips for `monaco-editor` when using the library.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
```
|
||||
|
||||
## Vite bundling tips
|
||||
- Monaco requires worker configuration for production builds. Use `vite-plugin-monaco-editor-esm` or `vite-plugin-monaco-editor`.
|
||||
- In Vite config:
|
||||
```ts
|
||||
import { defineConfig } from 'vite'
|
||||
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
|
||||
|
||||
export default defineConfig({ plugins: [monacoEditorPlugin()] })
|
||||
```
|
||||
|
||||
## Avoid worker-not-found errors
|
||||
- Ensure the plugin packages Monaco workers where your site expects them (e.g., via `customDistPath`) and verify worker file urls after build.
|
||||
- If bundling into a library, make sure the peer `stream-monaco` is installed in consuming applications.
|
||||
|
||||
## Packaging notes
|
||||
- The library lazy-loads Monaco only when a `CodeBlockNode` mounts. This reduces initial bundle size and avoids SSR failures.
|
||||
- If the host app wants to warm Monaco earlier, call markstream-vue's `preloadCodeBlockRuntime()` instead of importing `stream-monaco` directly. This keeps markstream-vue's code block runtime readiness in sync with the worker preload.
|
||||
|
||||
## Troubleshooting
|
||||
- If you see `Failed to load Monaco worker`, check that the worker files are present in `dist` and accessible by the built site. The plugin's `customDistPath` can help relocate them.
|
||||
|
||||
Quick try — preload the code block runtime at app startup so editor mounts faster and warm remounts skip the loading fallback:
|
||||
|
||||
```ts
|
||||
import { preloadCodeBlockRuntime } from 'markstream-vue'
|
||||
|
||||
void preloadCodeBlockRuntime()
|
||||
```
|
||||
|
||||
`getUseMonaco()` remains supported for existing integrations and has the same runtime-ready side effect.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Monaco Editor Integration
|
||||
|
||||
Monaco integration is provided by `stream-monaco` and is optional. It supports fast, incremental updates for large code blocks.
|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
```
|
||||
|
||||
Use `CodeBlockNode` (default) to render Monaco-powered code blocks. For read-only usage, use `MarkdownCodeBlockNode`.
|
||||
|
||||
Tips:
|
||||
- Defer Monaco initialization for offscreen code blocks
|
||||
- Use `codeBlockStream: false` to avoid partial updates if desired
|
||||
- Preload via `preloadCodeBlockRuntime()` during app idle or route setup when code blocks are likely
|
||||
- No additional CSS import is required
|
||||
|
||||

|
||||
|
||||
## Vue CLI (Webpack 4) limitation
|
||||
|
||||
`stream-monaco` relies on `import.meta.url` to locate Monaco worker assets, which **Webpack 4** cannot compile reliably. For Vue CLI 4 (Webpack 4):
|
||||
|
||||
- Prefer upgrading to Webpack 5 or migrating to Vite for the best Monaco experience.
|
||||
- If you must stay on Webpack 4, prefer switching code blocks to the Shiki-based renderer (`stream-markdown`) by overriding `code_block` (see [/guide/code-blocks](/guide/code-blocks)). You can also explicitly ignore Monaco deps in `vue.config.js`:
|
||||
|
||||
```js
|
||||
// vue.config.js
|
||||
const webpack = require('webpack')
|
||||
|
||||
module.exports = {
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /^stream-monaco$/ }),
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /^monaco-editor$/ }),
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Vite & worker setup
|
||||
|
||||
Monaco requires worker packaging for production builds. Use `vite-plugin-monaco-editor-esm` to ensure workers are bundled into your app's build output. Example config:
|
||||
|
||||
```ts
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import monacoEditorPlugin from 'vite-plugin-monaco-editor-esm'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
monacoEditorPlugin({
|
||||
languageWorkers: [
|
||||
'editorWorkerService',
|
||||
'typescript',
|
||||
'css',
|
||||
'html',
|
||||
'json',
|
||||
],
|
||||
customDistPath(root, buildOutDir, base) {
|
||||
return path.resolve(buildOutDir, 'monacoeditorwork')
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Preloading Monaco
|
||||
|
||||
To avoid a cold-start fallback flash when the first code block mounts, preload the code block runtime during app initialization, app idle time, or on first route mount:
|
||||
|
||||
```ts
|
||||
import { preloadCodeBlockRuntime } from 'markstream-vue'
|
||||
|
||||
void preloadCodeBlockRuntime()
|
||||
```
|
||||
|
||||
`preloadCodeBlockRuntime()` dynamically imports `stream-monaco`, registers Monaco workers through markstream-vue's runtime path, and marks the code block runtime as ready so later `CodeBlockNode` remounts can skip the `<pre>` loading fallback.
|
||||
|
||||
Existing code that calls `getUseMonaco()` can stay as-is; it now has the same runtime-ready side effect and still returns the loaded `stream-monaco` module or `null`.
|
||||
|
||||
### Webpack & MonacoWebpackPlugin
|
||||
|
||||
If your app bundles Monaco with `monaco-editor-webpack-plugin`, let the plugin own worker resolution via `globalThis.MonacoEnvironment`. `markstream-vue` will not override `MonacoEnvironment.getWorker/getWorkerUrl` when they already exist.
|
||||
|
||||
Quick try — Render a monaco-enabled code block (after installing `stream-monaco`):
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const node = { type: 'code_block', language: 'js', raw: 'console.log(123)', code: 'console.log(123)' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodeBlockNode :node="node" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Add extra languages & themes
|
||||
|
||||
Only a minimal set of Monaco languages ships with the default integration to keep the first render fast. When you need additional grammars (Rust, Go, Bash, etc.) or want to ship your own VS Code themes, pass them through `monacoOptions` — either directly on `CodeBlockNode` or globally via `MarkdownRender`'s `codeBlockMonacoOptions` prop. The object is forwarded to `useMonaco()` unchanged.
|
||||
|
||||
> `languages` is **not** appended to the built-in defaults from `stream-monaco`; providing this array replaces the internal `defaultLanguages`. Include every language you need (even the original ones) whenever you override it.
|
||||
|
||||
Only `ts twoslash` and `vue twoslash` fences in this docs site enable hoverable type details. Hover `languages`, `themes`, `theme`, `MAX_HEIGHT`, or `onDiffHunkAction` below for the useful payloads.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockMonacoOptions, CodeBlockMonacoTheme } from 'markstream-vue'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const docsDark: CodeBlockMonacoTheme = {
|
||||
name: 'docs-dark',
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
colors: {
|
||||
'editor.background': '#05060a',
|
||||
},
|
||||
rules: [],
|
||||
}
|
||||
|
||||
const docsLight: CodeBlockMonacoTheme = {
|
||||
name: 'docs-light',
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
colors: {
|
||||
'editor.background': '#ffffff',
|
||||
},
|
||||
rules: [],
|
||||
}
|
||||
|
||||
const monacoOptions = {
|
||||
languages: ['javascript', 'python', 'rust', 'shell'],
|
||||
themes: [docsDark, docsLight],
|
||||
theme: 'docs-dark',
|
||||
MAX_HEIGHT: 640,
|
||||
} satisfies CodeBlockMonacoOptions
|
||||
|
||||
const markdown = `
|
||||
\`\`\`python
|
||||
print("extra languages go here")
|
||||
\`\`\`
|
||||
|
||||
\`\`\`rust
|
||||
fn main() {}
|
||||
\`\`\`
|
||||
`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:content="markdown"
|
||||
:code-block-monaco-options="monacoOptions"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
> Each entry in `languages` can be a Monaco language id string or the loader signature that `stream-monaco` documents (for lazy language bundles). When not using `MarkdownRender`, pass the same `monacoOptions` object directly to `CodeBlockNode` via `:monaco-options`.
|
||||
|
||||
### Diff hover actions
|
||||
|
||||
Diff code blocks can show hover action buttons for each hunk split (`Revert` / `Stage`). These options are also passed through `monacoOptions` / `codeBlockMonacoOptions`:
|
||||
|
||||
```ts twoslash
|
||||
import type { CodeBlockDiffHunkActionContext, CodeBlockMonacoOptions } from 'markstream-vue'
|
||||
|
||||
const monacoOptions = {
|
||||
diffHunkActionsOnHover: true,
|
||||
diffHunkHoverHideDelayMs: 240,
|
||||
onDiffHunkAction(context: CodeBlockDiffHunkActionContext) {
|
||||
console.log(context.action, context.side, context.lineChange)
|
||||
// Return false to prevent stream-monaco's built-in model edits.
|
||||
return false
|
||||
},
|
||||
} satisfies CodeBlockMonacoOptions
|
||||
```
|
||||
|
||||
- `diffHunkActionsOnHover`: enable the hover buttons
|
||||
- `diffHunkHoverHideDelayMs`: control how long the hover widget stays visible after mouse leave
|
||||
- `onDiffHunkAction`: intercept `revert` / `stage` before the default edit runs
|
||||
|
||||
#### Full example
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const actionLogs = ref<string[]>([])
|
||||
|
||||
const monacoOptions = {
|
||||
diffHunkActionsOnHover: true,
|
||||
diffHunkHoverHideDelayMs: 240,
|
||||
onDiffHunkAction(context) {
|
||||
actionLogs.value = [
|
||||
`${context.action}:${context.side}`,
|
||||
...actionLogs.value,
|
||||
].slice(0, 6)
|
||||
// Prevent built-in edits so the demo stays stable while you inspect events.
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
const markdown = [
|
||||
'```diff json:package.json',
|
||||
'{',
|
||||
' "name": "markstream-vue",',
|
||||
'- "version": "0.0.49",',
|
||||
'+ "version": "0.0.54-beta.1",',
|
||||
' "packageManager": "pnpm@10.16.1"',
|
||||
'}',
|
||||
'```',
|
||||
].join('\\n')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="markdown"
|
||||
:code-block-monaco-options="monacoOptions"
|
||||
/>
|
||||
|
||||
<ul>
|
||||
<li v-for="(item, index) in actionLogs" :key="`${item}-${index}`">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
```
|
||||
|
||||
Hover the changed red/green hunk area to reveal the `Revert` / `Stage` buttons. Clicking either button will call `onDiffHunkAction`.
|
||||
|
||||
> The current action labels are `Revert` and `Stage` (not `Stash`).
|
||||
>
|
||||
> In `markstream-vue`, these options should be provided in the initial `monacoOptions` object used to create the editor. If you need to toggle them at runtime, remount the code block so the Monaco diff editor is recreated with the new options.
|
||||
@@ -0,0 +1,289 @@
|
||||
---
|
||||
description: Use the parser API to hook into tokens, AST transforms, and markdown-it customization before markstream-vue renders nodes.
|
||||
---
|
||||
|
||||
# Parser — API Deep Dive
|
||||
|
||||
This page extracts the most important functions, types and helpers from `packages/markdown-parser` for a focused API reference.
|
||||
|
||||
## Main functions
|
||||
|
||||
### `getMarkdown(msgId?, options?)`
|
||||
Create a configured `markdown-it-ts` instance.
|
||||
|
||||
Parameters:
|
||||
- `msgId` (string | optional): identifier for this instance
|
||||
- `options` (GetMarkdownOptions | optional): configuration options
|
||||
|
||||
Options include:
|
||||
- `plugin`: list of Markdown-it plugins
|
||||
- `apply`: functions to mutate the `MarkdownIt` instance
|
||||
- `i18n`: translator map or function
|
||||
- `customHtmlTags`: custom HTML-like tags treated as common during streaming mid‑state handling (e.g. `['thinking']`)
|
||||
|
||||
### `parseMarkdownToStructure(content, md, options?)`
|
||||
Parse a Markdown string into a streaming-friendly AST used by the renderer.
|
||||
|
||||
Parameters:
|
||||
- `content` (string): markdown to parse
|
||||
- `md` (MarkdownItCore): a markdown-it-ts instance created by `getMarkdown()`
|
||||
- `options` (ParseOptions, optional): contains transform hooks described below
|
||||
|
||||
> Tip for custom components: for simple HTML‑like tags such as `<thinking>`, prefer the built‑in `customHtmlTags` / `custom-html-tags` allowlist so the parser emits custom nodes directly. Use `preTransformTokens` only when you need to reshape `content`/`attrs`. See [custom component parsing](/guide/advanced#custom-component-parsing) for details.
|
||||
|
||||
Returns: `ParsedNode[]`
|
||||
|
||||
### ParseOptions: `final` (end-of-stream mode)
|
||||
|
||||
In streaming contexts (SSE / AI chat), the parser intentionally emits some **mid-state (`loading`) nodes** to reduce flicker and keep rendering stable while content is incomplete, for example:
|
||||
- Unclosed block math (starts with `$$` but the closing `$$` hasn’t arrived yet)
|
||||
- Unclosed code fences (only the opening ``` has arrived)
|
||||
|
||||
When you know the message is complete, switch the parser to “final” mode:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure(buffer, md, { final: true })
|
||||
```
|
||||
|
||||
What `final: true` does:
|
||||
- Disables mid-state loading behavior for unclosed constructs (so trailing `$$...` won’t produce perpetual `math_block/loading` nodes).
|
||||
- Treats EOF as an implicit closing fence so code blocks don’t get stuck in loading.
|
||||
|
||||
### `processTokens(tokens)`
|
||||
Converts raw markdown-it tokens into a processed token list ready for the AST phase.
|
||||
|
||||
### `parseInlineTokens(tokens, content?, preToken?, options?)`
|
||||
Parse inline tokens into inline nodes usable by the renderer.
|
||||
|
||||
## Configuration & helpers
|
||||
|
||||
### `setDefaultMathOptions(options)`
|
||||
Set global math rendering options for KaTeX. Example:
|
||||
|
||||
```ts
|
||||
setDefaultMathOptions({
|
||||
commands: ['infty', 'perp'],
|
||||
escapeExclamation: true
|
||||
})
|
||||
```
|
||||
|
||||
### Heuristics and utilities
|
||||
- `isMathLike(content)` — heuristic to detect whether a string looks like math
|
||||
- `findMatchingClose(src, startIdx, open, close)` — find matching delimiter
|
||||
- `parseFenceToken(token)` — parse code fence into `CodeBlockNode`
|
||||
- `normalizeStandaloneBackslashT(content, options?)` — normalize backslash-t sequences in math content
|
||||
|
||||
## Parse hooks (advanced)
|
||||
When calling `parseMarkdownToStructure` you can pass `ParseOptions` with these hooks:
|
||||
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — operate immediately after the `markdown-it` parser runs
|
||||
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — transform tokens after internal fixes
|
||||
|
||||
If you need to reshape the AST, post-process the returned `ParsedNode[]` and pass it to `MarkdownRender` via the `nodes` prop.
|
||||
|
||||
These hooks are also available via `parseOptions` prop on the `MarkdownRender` component (applies only when using `content` instead of `nodes`).
|
||||
|
||||
### Unknown HTML-like tags (default behavior)
|
||||
|
||||
Non-standard HTML-like tags (for example `<question>`) render as raw HTML elements once complete. Incomplete or malformed fragments are kept as **literal text** so they do not swallow surrounding content.
|
||||
|
||||
If you want a custom tag to be emitted as a custom node (so it can be mapped via `setCustomComponents` with parsed attrs/content), add it to `customHtmlTags`.
|
||||
|
||||
### ParseOptions: `requireClosingStrong`
|
||||
|
||||
`requireClosingStrong` (boolean | optional) controls how the parser treats unmatched `**` strong delimiters inside inline content. Default: `false` (streaming-friendly).
|
||||
|
||||
- **true**: The parser requires a matching closing `**` to create a strong node. Unclosed `**` are left as plain text. This is the recommended, strict mode for non-interactive rendering and avoids incorrect strong parsing inside constructs like link text (for example, `[**cxx](xxx)`).
|
||||
- **false**: The parser allows mid-state/unfinished `**` (useful for editor live-preview scenarios), which can produce a temporary strong node even when the closing `**` is missing.
|
||||
|
||||
Example — strict parsing (recommended for non-streaming):
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: true })
|
||||
// the text `[**cxx](xxx)` will be preserved without creating a dangling strong node
|
||||
```
|
||||
|
||||
Example — editor-friendly parsing:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: false })
|
||||
// allows creating a temporary/"mid-state" strong node for live-edit previews
|
||||
```
|
||||
|
||||
### ParseOptions: `validateLink`
|
||||
|
||||
`validateLink` (`(url: string) => boolean` | optional) controls whether a parsed link is emitted as a `link` node.
|
||||
|
||||
- Returns **true**: emit a normal `link` node.
|
||||
- Returns **false**: downgrade the link to plain text (keep link text only).
|
||||
|
||||
Priority and integration:
|
||||
- `parseMarkdownToStructure(..., { validateLink })` has highest priority.
|
||||
- If omitted, the parser reuses the rule from the MarkdownIt instance when available (for example via `md.set({ validateLink })` or `md.options.validateLink`).
|
||||
|
||||
Example — block unsafe schemes while keeping normal https links:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('safe-links')
|
||||
md.set?.({
|
||||
validateLink: (url: string) => !/^\s*javascript:/i.test(url.trim()),
|
||||
})
|
||||
|
||||
const nodes = parseMarkdownToStructure(
|
||||
'[safe](https://example.com) [unsafe](javascript:alert(1))',
|
||||
md,
|
||||
{ final: true },
|
||||
)
|
||||
// "safe" is emitted as a link node; "unsafe" is emitted as plain text
|
||||
```
|
||||
|
||||
### Stray `$$` delimiters (empty math)
|
||||
|
||||
Sometimes streams contain an accidental `$$` sequence, e.g.
|
||||
|
||||
```md
|
||||
adasd $$ dasdsa
|
||||
dasdsad
|
||||
```
|
||||
|
||||
If `$$` is misinterpreted as a math delimiter with empty content, the renderer can appear “stuck” because there is no math content to render. The parser now treats empty `$$` as plain text to avoid perpetual loading spinners.
|
||||
|
||||
### Streaming inline HTML mid-states (`html_inline`)
|
||||
|
||||
For streaming/chat/editor scenarios, inline HTML often arrives in partial chunks. The parser applies a conservative mid‑state strategy to reduce flicker while still rendering stable HTML as soon as possible.
|
||||
|
||||
Behavior:
|
||||
- **Suppress incomplete tags** in text tokens until `>` arrives. Examples of suppressed fragments: `<span class="a"`, trailing `<`, `</sp`, trailing `</`. These fragments are not emitted as plain text nodes to avoid jitter.
|
||||
- **Recognize complete open tags** for common HTML tags (conservative allowlist). Once `<tag ...>` is complete, it becomes an `html_inline` node.
|
||||
- **If no explicit closing tag exists yet**, the parser:
|
||||
- Treats all following inline tokens as that tag’s inner content.
|
||||
- Auto‑appends `</tag>` to `content/raw` so the fragment is valid HTML for rendering.
|
||||
- Marks the node as `loading: true` and `autoClosed: true` to indicate the source is still incomplete.
|
||||
- **When the real closing tag arrives**, `autoClosed` disappears and `loading` becomes `false`.
|
||||
|
||||
Extending the allowlist:
|
||||
To apply the same mid‑state suppression for custom tags (for example `<thinking>`), pass `customHtmlTags` when creating the markdown instance:
|
||||
|
||||
```ts
|
||||
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
|
||||
```
|
||||
|
||||
Emitting custom nodes:
|
||||
If you want those tags to become custom node types (so `setCustomComponents` can map them directly), also pass `customHtmlTags` in `ParseOptions` or use the `custom-html-tags` prop on `MarkdownRender` (which wires this automatically):
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
|
||||
const nodes = parseMarkdownToStructure(source, md, { customHtmlTags: ['thinking'] })
|
||||
```
|
||||
|
||||
Renderer note:
|
||||
- `HtmlInlineNode` renders raw text only when `loading === true` and `autoClosed !== true`.
|
||||
Auto‑closed mid‑states still render HTML (via `innerHTML`) but keep `loading=true` for UX/state.
|
||||
|
||||
## Types
|
||||
A condensed list of exported types to reference in your code:
|
||||
|
||||
- `CodeBlockNode`, `GetMarkdownOptions`, `HeadingNode`, `ListItemNode`, `ListNode`, `MathOptions`, `ParagraphNode`, `ParsedNode`, `ParseOptions`, etc.
|
||||
|
||||
Use `import type { ParsedNode, CodeBlockNode } from 'stream-markdown-parser'` in your TypeScript code.
|
||||
|
||||
## Plugins & Defaults
|
||||
This package includes a set of parsing helpers and convenience plugins for common flows (for example: footnotes, task checkboxes, sub/sup/mark). Note that emoji handling is no longer enabled by default — consumers who want emoji support should register the emoji plugin explicitly.
|
||||
|
||||
You can add custom plugins in several ways:
|
||||
- Pass plugins to `getMarkdown` via the `plugin` option.
|
||||
- Use `apply` functions in `getMarkdown` options to mutate the returned `MarkdownIt` instance.
|
||||
- When using the `MarkdownRender` component, use the `customMarkdownIt` prop to receive and mutate the `MarkdownIt` instance used for that renderer.
|
||||
|
||||
Example — enable emoji via the component prop:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { MarkdownIt } from 'markdown-it-ts'
|
||||
import { full as markdownItEmoji } from 'markdown-it-emoji'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
function enableEmoji(md: MarkdownIt) {
|
||||
md.use(markdownItEmoji)
|
||||
return md
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="source" :custom-markdown-it="enableEmoji" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Examples
|
||||
Use the playground to test custom‑tag parsing quickly. For simple tags like `<thinking>`, pass `customHtmlTags` (or `custom-html-tags` on the component) so the parser emits `type: 'thinking'` nodes automatically, then map them via `setCustomComponents`.
|
||||
|
||||
For full details and more examples, see `packages/markdown-parser/README.md` in the repository.
|
||||
|
||||
Try this — quick test of a parse option:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('Hello **world**', md)
|
||||
console.log(nodes)
|
||||
// Render with <MarkdownRender :nodes="nodes" />
|
||||
```
|
||||
|
||||
### Custom components and tag-like elements
|
||||
|
||||
Tag-like custom elements (for example `<MyWidget ...>...</MyWidget>`) produce complex `html_block`/inline token shapes that are often difficult to reconstruct reliably from the parsed AST using simple regex or string splicing. To reduce maintenance cost and avoid brittle post-processing, we recommend extracting those raw component strings before feeding the remaining content to the Markdown parser and rendering the extracted parts separately as Vue components.
|
||||
|
||||
Recommended approach:
|
||||
- Pre-scan the incoming Markdown content and extract custom-component blocks into a small map keyed by an id (or placeholder).
|
||||
- Replace the original markup in the Markdown string with a stable placeholder token (for example `[[CUSTOM:1]]`).
|
||||
- Let the Markdown parser run on the placeholder-bearing content and then render the AST as usual.
|
||||
- In your rendering layer, render placeholders by looking up the extracted component string and mounting the corresponding Vue component (or compile it via a lightweight renderer).
|
||||
|
||||
Benefits:
|
||||
- Avoids brittle AST post-processing for nested/tag-like HTML.
|
||||
- Keeps the Markdown parsing focused on Markdown semantics.
|
||||
- Allows you to control hydration and scope for custom components separately.
|
||||
|
||||
Example (simple sketch):
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
// 1) extract custom tags
|
||||
const extracted = new Map<string, string>()
|
||||
let id = 1
|
||||
const contentWithPlaceholders = source.replace(/<MyWidget[\s\S]*?<\/MyWidget>/g, (m) => {
|
||||
const key = `[[CUSTOM:${id++}]]`
|
||||
extracted.set(key, m)
|
||||
return key
|
||||
})
|
||||
|
||||
// 2) parse the markdown with placeholders
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure(contentWithPlaceholders, md)
|
||||
|
||||
// 3) render: when you encounter a placeholder node, mount your extracted component
|
||||
// Example pseudo-Vue render logic:
|
||||
// if (node.type === 'text' && extracted.has(node.content)) {
|
||||
// return h(CustomWrapper, { raw: extracted.get(node.content) })
|
||||
// }
|
||||
```
|
||||
|
||||
### Thinking blocks and small inline-rich fragments
|
||||
|
||||
If you only need lightweight rendering for short "thinking" fragments (for example AI assistant thinking traces), extract those fragments and parse them separately, then render with a dedicated `MarkdownRender` instance. Keep complex tag-like custom components extracted as shown above so they don’t get mixed into the main AST.
|
||||
|
||||
This hybrid approach minimizes fragile string-manipulation of the Markdown AST while giving you full control over custom component hydration and rendering scope.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Parser & API
|
||||
|
||||
markstream-vue builds on top of `markdown-it-ts` and exposes a streaming-friendly parser.
|
||||
|
||||
For the full parser API reference, see:
|
||||
- [/guide/parser-api](/guide/parser-api)
|
||||
- `stream-markdown-parser` README (parser-only package): https://www.npmjs.com/package/stream-markdown-parser
|
||||
|
||||
Highlights:
|
||||
|
||||
- `getMarkdown()` — create and configure a `markdown-it-ts` instance
|
||||
- `parseMarkdownToStructure(content, md)` — turn a Markdown string into parsed nodes used by `MarkdownRender`
|
||||
- `setDefaultMathOptions()` — global math options
|
||||
- Streaming inline HTML mid-states (`html_inline`) — suppress partial tags and auto-close unclosed inline HTML for flicker-free streaming.
|
||||
|
||||
If you only need the parser (no Vue), install and import from `stream-markdown-parser`. Otherwise, `markstream-vue` re-exports the same helpers for convenience.
|
||||
|
||||
Try this quickly — parse a small string and render the result in your app:
|
||||
|
||||
```ts
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('# Hello\n\nThis is a test', md)
|
||||
// Render nodes with <MarkdownRender :nodes="nodes" />
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
description: Improve markstream-vue performance for streaming chat UIs, large documents, Monaco code blocks, and heavy Mermaid or KaTeX content.
|
||||
---
|
||||
|
||||
# Performance Features & Tips
|
||||
|
||||
The renderer is optimized for streaming and large docs. Key features:
|
||||
|
||||
- Incremental parsing for code blocks
|
||||
- Efficient DOM updates and memory optimizations
|
||||
- Monaco streaming updates
|
||||
- Progressive Mermaid rendering
|
||||
|
||||
Performance tips:
|
||||
|
||||
- Stream long documents in chunks
|
||||
- Use `MarkdownCodeBlockNode` or `renderCodeBlocksAsPre` for non-editable code
|
||||
- Scope custom components to enable GC
|
||||
- Use `setDefaultMathOptions` at bootstrap
|
||||
|
||||
## Bundle size workflow (maintainers)
|
||||
|
||||
If you are changing code paths that can impact build size (renderers, code blocks, optional peers), run this flow before merging:
|
||||
|
||||
- `pnpm build:analyze` to produce visual reports (`bundle-visualizer.html` and `bundle-visualizer-tailwind.html`) and confirm what actually moved between chunks.
|
||||
- `pnpm size:check` to enforce local size budgets for `dist`, largest JS chunk, and `npm pack --dry-run` output.
|
||||
- Optional: tighten budgets in CI/locally with env vars like `MAX_DIST_BYTES`, `MAX_JS_CHUNK_BYTES`, `MAX_PACK_TGZ_BYTES`, `MAX_PACK_UNPACKED_BYTES`.
|
||||
|
||||
## Keeping a Steady, Typewriter-Style Stream
|
||||
|
||||
Some AI or LLM sources send content in large bursts, which can feel like the preview is freezing and then dumping a whole block. To keep the UI feeling like a smooth, continuous typewriter:
|
||||
|
||||
- **Keep `typewriter` enabled** on `MarkdownRender` (default) so non-code nodes fade in instead of popping instantly, and newly appended streamed text can replay a short local fade without dimming the whole block.
|
||||
- **Tune the batching props**: drop `initialRenderBatchSize`/`renderBatchSize` (for example `12`/`24`), and add a small `renderBatchDelay` (20–30 ms). Even if the model sends a huge chunk, the renderer then inserts tiny slices each frame, producing a stable flow.
|
||||
- **Throttle upstream updates** if possible: instead of replacing `content` on every incoming hunk, debounce (50–100 ms) or split into smaller paragraphs so each render cycle operates on a “bite-sized” diff.
|
||||
- **Defer heavy nodes** by keeping `deferNodesUntilVisible`/`viewportPriority` turned on; expensive blocks (Mermaid/Monaco) will wait until they are near the viewport so the stream of text is never blocked.
|
||||
- **Fall back for code blocks** when a burst happens: disable `codeBlockStream` or temporarily use `renderCodeBlocksAsPre` during streaming so that syntax-highlighting work does not stall text updates.
|
||||
|
||||
These knobs keep DOM work under a predictable budget, so users perceive a calm, steady flow of content even when the backend sends data in erratic bursts.
|
||||
|
||||
Try this — tune rendering performance by enabling `viewportPriority`:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Perf test'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" :viewport-priority="true" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Virtualization & DOM windows
|
||||
|
||||
`MarkdownRender` keeps a moving window of nodes in memory so extremely long documents stay responsive:
|
||||
|
||||
- `maxLiveNodes` (default `320`) caps how many fully rendered nodes remain in the DOM. Tune this based on your layout — lower values reduce memory but require slightly more placeholder churn; higher values prioritise scrollback.
|
||||
- `liveNodeBuffer` controls overscan on both sides of the focus window (default `60`). Increase it when nodes vary wildly in height to avoid visible pop-in while scrolling fast.
|
||||
- `deferNodesUntilVisible` together with `viewportPriority` defers mounting heavy nodes (Mermaid, Monaco, KaTeX) until an observer reports they are close to the viewport.
|
||||
- `batchRendering`, `initialRenderBatchSize`, `renderBatchSize`, `renderBatchDelay`, and `renderBatchBudgetMs` govern how many nodes switch from placeholders to full components per frame. This incremental mode only runs when virtualization is disabled (`:max-live-nodes="0"`); otherwise the virtual window already limits DOM work, so nodes are rendered immediately without placeholders.
|
||||
|
||||
Example: Give the user a lighter DOM footprint while keeping scrollback smooth.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Virtualized transcript'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
:max-live-nodes="220"
|
||||
:live-node-buffer="40"
|
||||
:batch-rendering="true"
|
||||
:initial-render-batch-size="24"
|
||||
:render-batch-size="48"
|
||||
:render-batch-delay="24"
|
||||
:render-batch-budget-ms="8"
|
||||
:defer-nodes-until-visible="true"
|
||||
:viewport-priority="true"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
With these knobs you can keep large AI transcripts or docs under a predictable CPU budget while still presenting consistent scroll behaviour.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: Explore local and online markstream-vue playgrounds to reproduce rendering issues, compare frameworks, and verify fixes quickly.
|
||||
---
|
||||
|
||||
# Playground
|
||||
|
||||
This repository includes a playable demo under `/playground` that helps you test features locally and reproduce rendering issues.
|
||||
|
||||
## Online Playgrounds
|
||||
|
||||
- Vue 3: https://markstream-vue.simonhe.me/
|
||||
- React: https://markstream-react.pages.dev/
|
||||
- React migration demo: https://markstream-react.pages.dev/migration-demo
|
||||
- Nuxt: https://markstream-nuxt.pages.dev/
|
||||
- Vue 2: https://markstream-vue2.pages.dev/
|
||||
|
||||
## Running Locally
|
||||
|
||||
How to run the playground locally:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm play
|
||||
# Open the dev server shown in terminal (usually http://localhost:5173)
|
||||
```
|
||||
|
||||
The playground demonstrates:
|
||||
|
||||
- Live streaming Markdown input with progressive Mermaid diagrams
|
||||
- Custom components mapping and `setCustomComponents`
|
||||
- Monaco streaming integration and code block examples
|
||||
|
||||
Example pages (open from the playground):
|
||||
|
||||
- `https://markstream-vue.simonhe.me/test` — shareable repro page for stress-testing streaming features (hosted demo)
|
||||
- `https://markstream-react.pages.dev/migration-demo` — hosted before/after migration demo for `react-markdown` users
|
||||
- `/markdown` — Markdown vs static rendering comparison
|
||||
|
||||

|
||||
|
||||
If you want to add examples to the playground for documentation pages, keep them minimal and focused (the `playground/src/pages` folder is a good place to add pages).
|
||||
|
||||
Quick try — add a simple playground page by creating `playground/src/pages/quick-test.vue` and pasting a small test like:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Quick playground test\n\nThis is a demo.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
description: Fine-tune markstream-vue with MarkdownRender props for streaming, dark mode, custom tags, parser options, and heavy-node control.
|
||||
---
|
||||
|
||||
# Component props & options
|
||||
|
||||
Use this page when you need to fine-tune streaming behaviour, control heavy nodes, or understand how `MarkdownRender` interacts with Tailwind/UnoCSS projects.
|
||||
|
||||
## Core props on `MarkdownRender`
|
||||
|
||||
| Prop | Type | Default | Notes |
|
||||
| ---- | ---- | ------- | ----- |
|
||||
| `content` | `string` | – | Raw Markdown string (required unless `nodes` is provided). |
|
||||
| `nodes` | `BaseNode[]` | – | Pre-parsed AST structure (typically `ParsedNode[]` from `parseMarkdownToStructure`). Skip this when you want the component to parse internally. |
|
||||
| `custom-id` | `string` | – | Scopes `setCustomComponents` mappings and lets you target CSS via `[data-custom-id="..."]`. |
|
||||
| `is-dark` | `boolean` | `false` | Theme flag forwarded to heavy nodes (Mermaid/D2/KaTeX/CodeBlock). Also adds `.dark` on the root container. |
|
||||
| `index-key` | `number \| string` | – | Key prefix for internal node keys; set this when nesting or rendering multiple MarkdownRender instances in the same list. |
|
||||
| `final` | `boolean` | `false` | Marks the input as end-of-stream. Disables streaming mid-state (loading) parsing so trailing delimiters (like `$$` or an unclosed code fence) won’t get stuck in a perpetual loading state. |
|
||||
| `parse-options` | `ParseOptions` | – | Token hooks (`preTransformTokens`, `postTransformTokens`). Applies only when `content` is provided. |
|
||||
| `custom-html-tags` | `string[]` | – | Extra HTML-like tags treated as common during streaming mid‑states and emitted as custom nodes (`type: 'thinking'`, etc.) for `setCustomComponents` mapping (forwarded to `getMarkdown`, e.g. `['thinking']`). |
|
||||
| `custom-markdown-it` | `(md: MarkdownIt) => MarkdownIt` | – | Customize the internal MarkdownIt instance (add plugins, tweak options). |
|
||||
| `debug-performance` | `boolean` | `false` | Logs parse/render timing and virtualization stats (dev only). |
|
||||
| `typewriter` | `boolean` | `true` | Enables the subtle enter animation. Disable if you need zero animation for SSR snapshots. |
|
||||
|
||||
## Streaming & heavy-node toggles
|
||||
|
||||
| Flag | Default | What it does |
|
||||
| ---- | ------- | ------------ |
|
||||
| `render-code-blocks-as-pre` | `false` | Render non‑Mermaid/Infographic/D2 `code_block` nodes as `<pre><code>` (uses `PreCodeNode`). Mermaid/infographic/D2 blocks still route to their dedicated nodes unless you override them via `setCustomComponents`. |
|
||||
| `code-block-stream` | `true` | Stream code blocks as content arrives. Disable to keep Monaco in a loading state until the final chunk lands—useful when incomplete code causes parser hiccups. |
|
||||
| `viewport-priority` | `true` | Defers heavy work (Monaco, Mermaid, D2, KaTeX) when elements are offscreen. Turn off if you need deterministic renders for PDF/print pipelines. |
|
||||
| `defer-nodes-until-visible` | `true` | When enabled, heavy nodes can render as placeholders until they approach the viewport (non-virtualized mode only). |
|
||||
|
||||
## Rendering performance (virtualization & batching)
|
||||
|
||||
| Prop | Default | Notes |
|
||||
| ---- | ------- | ----- |
|
||||
| `max-live-nodes` | `320` | Virtualization threshold; set `0` to disable virtualization (renders everything). |
|
||||
| `live-node-buffer` | `60` | Overscan window (how many nodes to keep before/after the focus range). |
|
||||
| `batch-rendering` | `true` | Incremental rendering batches (only when `max-live-nodes <= 0`). |
|
||||
| `initial-render-batch-size` | `40` | How many nodes render immediately before batching begins. |
|
||||
| `render-batch-size` | `80` | How many nodes render per batch tick. |
|
||||
| `render-batch-delay` | `16` | Extra delay (ms) before each batch after rAF. |
|
||||
| `render-batch-budget-ms` | `6` | Time budget (ms) before adaptive batch sizes shrink. |
|
||||
| `render-batch-idle-timeout-ms` | `120` | Timeout (ms) for `requestIdleCallback` slices (when available). |
|
||||
|
||||
## Global code block options (forwarded from `MarkdownRender`)
|
||||
|
||||
These props are forwarded to `CodeBlockNode` / `MarkdownCodeBlockNode` (but **not** to Mermaid/D2/Infographic blocks, which route to their dedicated nodes):
|
||||
|
||||
- `code-block-dark-theme`, `code-block-light-theme`
|
||||
- `code-block-monaco-options`
|
||||
- `code-block-min-width`, `code-block-max-width`
|
||||
- `code-block-props` (extra code-block props such as `showHeader`, `showFontSizeButtons`, and `showTooltips`, plus any custom forwarded keys)
|
||||
- `themes` (theme list forwarded to `stream-monaco` when present)
|
||||
|
||||
Note: `code-block-monaco-options` is only used by the Monaco-backed `CodeBlockNode`. If you override `code_block` with `MarkdownCodeBlockNode`, treat `code-block-dark-theme` / `code-block-light-theme` as Shiki theme names, and `themes` as the Shiki theme list to preload.
|
||||
|
||||
Only `ts twoslash` and `vue twoslash` fences in this docs site enable hoverable type details. Hover the object keys below, or `:code-block-monaco-options` in the template, instead of the imported type name.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockMonacoOptions } from 'markstream-vue'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '```ts\nconsole.log("hover monaco options")\n```'
|
||||
const monacoOptions = {
|
||||
themes: ['vitesse-dark', 'vitesse-light'],
|
||||
languages: ['typescript', 'vue', 'json'],
|
||||
theme: 'vitesse-dark',
|
||||
MAX_HEIGHT: 640,
|
||||
diffHideUnchangedRegions: {
|
||||
enabled: true,
|
||||
contextLineCount: 2,
|
||||
},
|
||||
diffHunkActionsOnHover: true,
|
||||
diffHunkHoverHideDelayMs: 240,
|
||||
} satisfies CodeBlockMonacoOptions
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:content="md"
|
||||
:code-block-monaco-options="monacoOptions"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
`code-block-props` is also strongly typed through the renderer props surface, so you can reuse it without falling back to `any`:
|
||||
|
||||
```ts twoslash
|
||||
import type { NodeRendererProps } from 'markstream-vue'
|
||||
|
||||
const codeBlockProps: NonNullable<NodeRendererProps['codeBlockProps']> = {
|
||||
showHeader: false,
|
||||
showFontSizeButtons: false,
|
||||
showTooltips: false,
|
||||
}
|
||||
```
|
||||
|
||||
## Diagram node props forwarded from `MarkdownRender`
|
||||
|
||||
Use these to control specialized block renderers without overriding components manually:
|
||||
|
||||
- `mermaid-props` forwards props to `MermaidBlockNode`
|
||||
- `d2-props` forwards props to `D2BlockNode`
|
||||
- `infographic-props` forwards props to `InfographicBlockNode`
|
||||
|
||||
For Mermaid and Infographic fences, `MarkdownRender` injects an `estimatedPreviewHeightPx` value when the caller does not provide one. This reserves a stable first-preview height for async loading and remounts. Custom `mermaid` and `infographic` renderers receive the same prop, so forward or consume it if the custom block renders its own preview shell.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
:mermaid-props="{ showHeader: false, renderDebounceMs: 180, previewPollDelayMs: 500 }"
|
||||
:d2-props="{ progressiveIntervalMs: 450, showCopyButton: false }"
|
||||
/>
|
||||
```
|
||||
|
||||
`mermaid-props` is especially useful for streaming tuning. Common keys include:
|
||||
|
||||
- `renderDebounceMs`
|
||||
- `contentStableDelayMs`
|
||||
- `previewPollDelayMs`
|
||||
- `previewPollMaxDelayMs`
|
||||
- `previewPollMaxAttempts`
|
||||
- toolbar toggles such as `showHeader`, `showModeToggle`, `showExportButton`, `showZoomControls`
|
||||
|
||||
## Code block header controls
|
||||
|
||||
Pass these props directly to `CodeBlockNode` / `MarkdownCodeBlockNode / MermaidBlockNode`, or globally via `code-block-props` on `MarkdownRender`:
|
||||
|
||||
- `show-header`
|
||||
- `show-copy-button`
|
||||
- `show-expand-button`
|
||||
- `show-preview-button`
|
||||
- `show-collapse-button`
|
||||
- `show-font-size-buttons`
|
||||
- `show-tooltips` (global tooltip switch for `LinkNode` + code-block nodes + mermaid-block nodes)
|
||||
|
||||
See `/guide/codeblock-header` and the `CodeBlockNode` types for the exhaustive list.
|
||||
|
||||
Example (global defaults):
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
:code-block-props="{ showHeader: false, showFontSizeButtons: false, showTooltips: false }"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Quick example
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Title\n\nSome content here.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
custom-id="docs"
|
||||
:viewport-priority="true"
|
||||
:code-block-stream="true"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Styling & troubleshooting reminders
|
||||
|
||||
1. **Import a reset first** (`modern-css-reset`, `@tailwind base`, or `@unocss/reset`), then wrap `markstream-vue/index.css` inside `@layer components` so Tailwind/Uno utilities don’t override node styles. See the [Tailwind guide](/guide/tailwind) for concrete snippets.
|
||||
2. **Scope overrides** with `custom-id` and `[data-custom-id="docs"]` selectors.
|
||||
3. **Confirm peer CSS** (KaTeX) is imported; Mermaid/D2 do not require extra CSS.
|
||||
4. **Check the [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here)** whenever visuals look off.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: Start rendering Markdown with markstream-vue in the smallest possible Vue example, including the default CSS behavior and next steps.
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
A minimal example using the library:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||
|
||||
const md: MarkdownRenderProps['content'] = `# Hello World\n\nThis is **bold** and this is *italic*.`
|
||||
const customId: MarkdownRenderProps['customId'] = 'quick-start'
|
||||
const isDark: MarkdownRenderProps['isDark'] = false
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
:content="md"
|
||||
:custom-id="customId"
|
||||
:is-dark="isDark"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
If you want prop-level hover, start with `MarkdownRenderProps['content']`, `MarkdownRenderProps['customId']`, `MarkdownRenderProps['isDark']`, or the matching template attributes above. Hovering the component name itself is usually less informative in Vue snippets.
|
||||
|
||||
Note: the packaged CSS is scoped under an internal `.markstream-vue` container to reduce global style conflicts. The main package entry already imports the default stylesheet; add `import 'markstream-vue/index.css'` only when you want explicit control over CSS order in your app shell.
|
||||
|
||||
For dark theme variables, either add `.dark` on an ancestor or pass `:is-dark="true"` to scope dark mode to the renderer.
|
||||
|
||||
Optional: wrap with `<client-only>` for Nuxt/SSR.
|
||||
|
||||
See `/nuxt-ssr` for Nuxt-specific instructions.
|
||||
|
||||
Try this quickly in your app:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||
|
||||
const md: MarkdownRenderProps['content'] = `# Hello world\n\nTry a simple Mermaid:\n\n\`\`\`mermaid\ngraph LR\nA-->B\n\`\`\`\n\nTry a simple D2:\n\n\`\`\`d2\ndirection: right\nClient -> API: request\nAPI -> DB: query\nDB -> API: rows\nAPI -> Client: response\n\`\`\`\n`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Install `mermaid` or `@terrastruct/d2` to render those diagrams; without them the renderer falls back to showing source text.
|
||||
|
||||
For high-frequency streaming, the highest-throughput setup is usually: parse outside the component and pass `:nodes` into `MarkdownRender`, instead of reparsing the entire `content` string every token. See `/guide/usage` and `/guide/performance`.
|
||||
@@ -0,0 +1,749 @@
|
||||
# React Components & API
|
||||
|
||||
markstream-react provides the same powerful components as markstream-vue, but built for React. All components support React 18+ with full TypeScript support.
|
||||
|
||||
The root `markstream-react`, `markstream-react/next`, and `markstream-react/server` entrypoints all ship declaration files. Shared renderer and component types such as `NodeRendererProps`, `NodeRendererCodeBlockProps`, `NodeComponentProps`, `RenderContext`, `RenderNodeFn`, `CustomComponentMap`, `CodeBlockMonacoOptions`, `MarkdownCodeBlockNodeProps`, `ListItemNodeProps`, `HtmlPreviewFrameProps`, `TooltipProps`, `TooltipPlacement`, and `LinkNodeStyleProps` can be imported directly from the entrypoint you use.
|
||||
## Main Component: MarkdownRender
|
||||
|
||||
The primary component for rendering markdown content in React.
|
||||
|
||||
### Props
|
||||
|
||||
`MarkdownRender` uses the `NodeRendererProps` interface from `markstream-react`.
|
||||
|
||||
#### Core props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `string` | - | Markdown content to render |
|
||||
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]` from the parser) |
|
||||
| `customId` | `string` | - | Identifier for scoping custom components and CSS |
|
||||
| `final` | `boolean` | `false` | Marks end-of-stream; stops emitting streaming `loading` nodes |
|
||||
| `parseOptions` | `ParseOptions` | - | Parser options and token hooks (only when `content` is provided) |
|
||||
| `customHtmlTags` | `readonly string[]` | - | HTML-like tags emitted as custom nodes (e.g. `thinking`) |
|
||||
| `customMarkdownIt` | `(md: MarkdownIt) => MarkdownIt` | - | Customize the internal MarkdownIt instance |
|
||||
| `debugPerformance` | `boolean` | `false` | Log parse/render timing and virtualization stats (dev only) |
|
||||
| `isDark` | `boolean` | `false` | Theme flag forwarded to heavy nodes; adds `.dark` to the root container |
|
||||
| `indexKey` | `number \| string` | - | Key prefix when rendering multiple instances in lists |
|
||||
| `typewriter` | `boolean` | `true` | Enable the non-code-node enter transition |
|
||||
| `showTooltips` | `boolean` | `true` | Global tooltip switch for `LinkNode` and code block nodes |
|
||||
|
||||
#### Streaming & heavy-node toggles
|
||||
|
||||
| Prop | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `renderCodeBlocksAsPre` | `false` | Render code blocks as `<pre><code>` (Mermaid/D2/Infographic blocks will also fall back) |
|
||||
| `codeBlockStream` | `true` | Stream code block updates as content arrives |
|
||||
| `viewportPriority` | `true` | Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport |
|
||||
| `deferNodesUntilVisible` | `true` | Render heavy nodes as placeholders until visible (non-virtualized mode only) |
|
||||
|
||||
#### Performance (virtualization & batching)
|
||||
|
||||
| Prop | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `maxLiveNodes` | `320` | Max fully rendered nodes kept in DOM (set `0` to disable virtualization) |
|
||||
| `liveNodeBuffer` | `60` | Overscan buffer around the focus range |
|
||||
| `batchRendering` | `true` | Incremental batch rendering when virtualization is disabled |
|
||||
| `initialRenderBatchSize` | `40` | Nodes rendered immediately before batching starts |
|
||||
| `renderBatchSize` | `80` | Nodes rendered per batch tick |
|
||||
| `renderBatchDelay` | `16` | Extra delay (ms) before each batch after rAF |
|
||||
| `renderBatchBudgetMs` | `6` | Time budget (ms) before adaptive batch sizes shrink |
|
||||
| `renderBatchIdleTimeoutMs` | `120` | Timeout (ms) for `requestIdleCallback` slices |
|
||||
|
||||
#### Global code block options
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `codeBlockDarkTheme` | `CodeBlockMonacoTheme` | Monaco dark theme object forwarded to every `CodeBlockNode` |
|
||||
| `codeBlockLightTheme` | `CodeBlockMonacoTheme` | Monaco light theme object forwarded to every `CodeBlockNode` |
|
||||
| `codeBlockMonacoOptions` | `CodeBlockMonacoOptions` | Options forwarded to `stream-monaco`, including diff hover-action settings like `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` |
|
||||
| `codeBlockMinWidth` | `string \| number` | Min width forwarded to `CodeBlockNode` |
|
||||
| `codeBlockMaxWidth` | `string \| number` | Max width forwarded to `CodeBlockNode` |
|
||||
| `codeBlockProps` | `NodeRendererCodeBlockProps` | Extra props forwarded to every code-block renderer (`CodeBlockNode` / `MarkdownCodeBlockNode`) |
|
||||
| `mermaidProps` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to Mermaid fences and custom `mermaid` renderers |
|
||||
| `d2Props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to D2 fences and custom `d2` renderers |
|
||||
| `infographicProps` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to infographic fences and custom `infographic` renderers |
|
||||
| `themes` | `CodeBlockMonacoTheme[]` | Theme list forwarded to `stream-monaco` |
|
||||
|
||||
#### Heavy renderer prop forwarding
|
||||
|
||||
`NodeRenderer` can tune heavy blocks directly:
|
||||
|
||||
```tsx
|
||||
<MarkdownRender
|
||||
content={markdown}
|
||||
viewportPriority
|
||||
mermaidProps={{
|
||||
showHeader: false,
|
||||
renderDebounceMs: 180,
|
||||
previewPollDelayMs: 500,
|
||||
}}
|
||||
d2Props={{ progressiveIntervalMs: 500 }}
|
||||
infographicProps={{ showHeader: false }}
|
||||
/>
|
||||
```
|
||||
|
||||
Streaming notes:
|
||||
- Keep `viewportPriority` enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
|
||||
- For high-frequency SSE, prefer passing `nodes` instead of reparsing the full `content` string every chunk.
|
||||
- Common Mermaid tuning keys: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`.
|
||||
|
||||
`NodeRendererCodeBlockProps` follows the public `CodeBlockNode` prop surface except for `node`, so you get completion for flags like `showHeader`, `showFontSizeButtons`, and `showTooltips` without dropping to `any`.
|
||||
|
||||
```tsx
|
||||
import type { NodeRendererCodeBlockProps } from 'markstream-react'
|
||||
|
||||
const codeBlockProps: NodeRendererCodeBlockProps = {
|
||||
showHeader: false,
|
||||
showFontSizeButtons: false,
|
||||
showTooltips: false,
|
||||
}
|
||||
```
|
||||
|
||||
#### Events
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `onCopy` | `(code: string) => void` | Fired when code blocks emit copy events |
|
||||
| `onHandleArtifactClick` | `(payload: any) => void` | Fired on artifact/preview clicks |
|
||||
| `onClick` | `(event: React.MouseEvent<HTMLDivElement>) => void` | Click handler for the renderer root |
|
||||
| `onMouseOver` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseover handler for the renderer root |
|
||||
| `onMouseOut` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseout handler for the renderer root |
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const markdown = `# Hello React!
|
||||
|
||||
This is markstream-react.`
|
||||
|
||||
return (
|
||||
<MarkdownRender
|
||||
customId="docs"
|
||||
content={markdown}
|
||||
maxLiveNodes={150}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Code Block Components
|
||||
|
||||
### MarkdownCodeBlockNode
|
||||
|
||||
Lightweight code highlighting using Shiki.
|
||||
|
||||
```tsx
|
||||
import { MarkdownCodeBlockNode } from 'markstream-react'
|
||||
|
||||
function CodeBlock() {
|
||||
const codeNode = {
|
||||
type: 'code_block',
|
||||
language: 'javascript',
|
||||
code: 'const hello = "world"',
|
||||
raw: 'const hello = "world"'
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
alert('Code copied!')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MarkdownCodeBlockNode
|
||||
node={codeNode}
|
||||
showCopyButton={true}
|
||||
showCollapseButton={false}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### CodeBlockNode
|
||||
|
||||
Feature-rich Monaco-powered code blocks.
|
||||
|
||||
```tsx
|
||||
import { CodeBlockNode } from 'markstream-react'
|
||||
|
||||
function MonacoCodeBlock() {
|
||||
const codeNode = {
|
||||
type: 'code_block',
|
||||
language: 'typescript',
|
||||
code: 'const greeting: string = "Hello"',
|
||||
raw: 'const greeting: string = "Hello"'
|
||||
}
|
||||
|
||||
const handleCopy = (code: string) => {
|
||||
console.log('Code copied:', code)
|
||||
}
|
||||
|
||||
const handlePreviewCode = (artifact: any) => {
|
||||
console.log('Preview code:', artifact)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<CodeBlockNode
|
||||
node={codeNode}
|
||||
monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
|
||||
stream={true}
|
||||
showCollapseButton={false}
|
||||
onCopy={handleCopy}
|
||||
onPreviewCode={handlePreviewCode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Math Components
|
||||
|
||||
### MathBlockNode
|
||||
|
||||
Renders block-level math formulas with KaTeX.
|
||||
|
||||
```tsx
|
||||
import { MathBlockNode } from 'markstream-react'
|
||||
|
||||
function MathBlock() {
|
||||
const mathNode = {
|
||||
type: 'math_block',
|
||||
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
||||
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MathBlockNode node={mathNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### MathInlineNode
|
||||
|
||||
Renders inline math formulas.
|
||||
|
||||
```tsx
|
||||
import { MathInlineNode } from 'markstream-react'
|
||||
|
||||
function MathInline() {
|
||||
const inlineMathNode = {
|
||||
type: 'math_inline',
|
||||
content: 'E = mc^2',
|
||||
raw: 'E = mc^2'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<p>
|
||||
The formula is:
|
||||
{' '}
|
||||
<MathInlineNode node={inlineMathNode} />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
### MermaidBlockNode
|
||||
|
||||
Progressive Mermaid diagram rendering.
|
||||
|
||||
#### Quick reference
|
||||
- **Tooltip control**: `showTooltips` defaults to `true`; set `false` to disable header action tooltips.
|
||||
|
||||
```tsx
|
||||
import { MermaidBlockNode } from 'markstream-react'
|
||||
|
||||
function MermaidDiagram() {
|
||||
const mermaidNode = {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: `graph TD
|
||||
A[Start] --> B{Is it working?}
|
||||
B -->|Yes| C[Great!]`,
|
||||
raw: ''
|
||||
}
|
||||
|
||||
const handleExport = (ev: any) => {
|
||||
console.log('Mermaid SVG:', ev.svgString)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MermaidBlockNode
|
||||
node={mermaidNode}
|
||||
isStrict={true}
|
||||
onExport={handleExport}
|
||||
showTooltips={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Event notes:
|
||||
- `onCopy(code: string)` receives the source text directly (no `MermaidBlockEvent` wrapper in React).
|
||||
- `onExport`, `onOpenModal`, `onToggleMode` receive `MermaidBlockEvent` and support `ev.preventDefault()` to stop the default behavior.
|
||||
- `onToggleMode` signature: `(target: 'source' | 'preview', ev)`.
|
||||
- For renderer-wide usage, prefer passing these knobs through `mermaidProps` on `MarkdownRender` / `NodeRenderer`.
|
||||
|
||||
## D2 Diagrams
|
||||
|
||||
### D2BlockNode
|
||||
|
||||
Progressive D2 diagram rendering with source fallback.
|
||||
|
||||
```tsx
|
||||
import { D2BlockNode } from 'markstream-react'
|
||||
|
||||
function D2Diagram() {
|
||||
const d2Node = {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: `direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query`,
|
||||
raw: ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<D2BlockNode
|
||||
node={d2Node}
|
||||
progressiveIntervalMs={600}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Other Node Components
|
||||
|
||||
### HeadingNode
|
||||
|
||||
```tsx
|
||||
import { HeadingNode } from 'markstream-react'
|
||||
|
||||
function CustomHeading() {
|
||||
const headingNode = {
|
||||
type: 'heading',
|
||||
level: 1
|
||||
}
|
||||
|
||||
return <HeadingNode node={headingNode}>Hello World</HeadingNode>
|
||||
}
|
||||
```
|
||||
|
||||
### ParagraphNode
|
||||
|
||||
```tsx
|
||||
import { ParagraphNode } from 'markstream-react'
|
||||
|
||||
function CustomParagraph() {
|
||||
const paragraphNode = {
|
||||
type: 'paragraph'
|
||||
}
|
||||
|
||||
return (
|
||||
<ParagraphNode node={paragraphNode}>
|
||||
This is a
|
||||
{' '}
|
||||
<strong>bold</strong>
|
||||
{' '}
|
||||
word.
|
||||
</ParagraphNode>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### ListNode
|
||||
|
||||
```tsx
|
||||
import { ListNode, renderNode } from 'markstream-react'
|
||||
|
||||
function CustomList() {
|
||||
const listNode = {
|
||||
type: 'list',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
children: [
|
||||
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 1' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list_item',
|
||||
children: [
|
||||
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 2' }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const ctx = { events: {} }
|
||||
|
||||
return <ListNode node={listNode} ctx={ctx} renderNode={renderNode} />
|
||||
}
|
||||
```
|
||||
|
||||
### LinkNode
|
||||
|
||||
```tsx
|
||||
import { LinkNode } from 'markstream-react'
|
||||
|
||||
function CustomLink() {
|
||||
const linkNode = {
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
title: 'Example',
|
||||
text: 'Click me'
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkNode
|
||||
node={linkNode}
|
||||
color="#e11d48"
|
||||
underlineHeight={3}
|
||||
showTooltip={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### ImageNode
|
||||
|
||||
```tsx
|
||||
import { ImageNode } from 'markstream-react'
|
||||
|
||||
function CustomImage() {
|
||||
const imageNode = {
|
||||
type: 'image',
|
||||
src: 'https://example.com/image.jpg',
|
||||
alt: 'Example image',
|
||||
title: 'Example',
|
||||
raw: ''
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('Image clicked!')
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
console.log('Image loaded!')
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageNode
|
||||
node={imageNode}
|
||||
onClick={handleClick}
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### getMarkdown
|
||||
|
||||
Get a configured markdown-it instance.
|
||||
|
||||
```tsx
|
||||
import { getMarkdown } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('my-msg-id', {
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
})
|
||||
|
||||
const tokens = md.parse('# Hello World')
|
||||
```
|
||||
|
||||
### parseMarkdownToStructure
|
||||
|
||||
Parse markdown string to AST structure.
|
||||
|
||||
```tsx
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)
|
||||
|
||||
// Use with MarkdownRender
|
||||
// <MarkdownRender nodes={nodes} />
|
||||
```
|
||||
|
||||
### Optional workers (Mermaid / KaTeX)
|
||||
|
||||
Mermaid/KaTeX auto-load when installed. If you want off‑thread parsing/rendering, inject workers:
|
||||
|
||||
```tsx
|
||||
import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
|
||||
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
|
||||
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'
|
||||
|
||||
setMermaidWorker(new MermaidWorker())
|
||||
setKaTeXWorker(new KatexWorker())
|
||||
```
|
||||
|
||||
## Custom Component API
|
||||
|
||||
### Props Interface
|
||||
|
||||
All custom node components receive these props:
|
||||
|
||||
```tsx
|
||||
import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'
|
||||
|
||||
interface NodeComponentProps<TNode = unknown> {
|
||||
node: TNode // The parsed node data
|
||||
ctx?: RenderContext // Renderer context (themes, events, flags)
|
||||
renderNode?: RenderNodeFn // Helper to render child nodes
|
||||
indexKey?: React.Key // Unique key for the node
|
||||
customId?: string // Custom ID for scoping
|
||||
isDark?: boolean
|
||||
typewriter?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
`CustomComponentMap` is the typed mapping shape accepted by `setCustomComponents(...)`.
|
||||
|
||||
### Example Custom Component
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
interface CustomParagraphProps {
|
||||
node: {
|
||||
type: string
|
||||
children: Array<{
|
||||
type: string
|
||||
content?: string
|
||||
children?: any[]
|
||||
}>
|
||||
}
|
||||
indexKey?: number | string
|
||||
customId?: string
|
||||
}
|
||||
|
||||
function CustomParagraph({ node, indexKey, customId }: CustomParagraphProps) {
|
||||
return (
|
||||
<p
|
||||
className={`custom-paragraph custom-paragraph-${indexKey}`}
|
||||
data-custom-id={customId}
|
||||
data-node-type={node.type}
|
||||
>
|
||||
{node.children.map((child, i) => (
|
||||
<span key={i}>{child.content || ''}</span>
|
||||
))}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
function App() {
|
||||
const paragraphNode = {
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'text', content: 'Custom paragraph content' }
|
||||
]
|
||||
}
|
||||
|
||||
return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
|
||||
}
|
||||
```
|
||||
|
||||
## Context + Custom Components
|
||||
|
||||
You can use React Context inside custom node components, while still registering them via `setCustomComponents`:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const ThemeContext = createContext<'light' | 'dark'>('light')
|
||||
|
||||
function CustomHeading({ node, customId }: any) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const level = node.level || 1
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className={`custom-heading ${theme}`} data-custom-id={customId}>
|
||||
{node.children?.map((child: any, i: number) => (
|
||||
<span key={i}>{child.content || ''}</span>
|
||||
))}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
setCustomComponents('docs', { heading: CustomHeading })
|
||||
|
||||
function App() {
|
||||
const markdown = `# Custom Heading
|
||||
|
||||
This uses a custom heading component.
|
||||
`
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value="dark">
|
||||
<MarkdownRender customId="docs" content={markdown} />
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming Support
|
||||
|
||||
markstream-react supports streaming markdown content:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function StreamingDemo() {
|
||||
const [content, setContent] = useState('')
|
||||
const fullContent = `# Streaming Demo
|
||||
|
||||
This content streams in **small chunks**.
|
||||
`
|
||||
|
||||
useEffect(() => {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i < fullContent.length) {
|
||||
setContent(prev => prev + fullContent[i])
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 30)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <MarkdownRender content={content} />
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
markstream-react includes full TypeScript definitions:
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps, NodeRendererProps } from 'markstream-react'
|
||||
import type { ParsedNode } from 'stream-markdown-parser'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const markdown = '# Hello TypeScript!'
|
||||
const nodes: ParsedNode[] = []
|
||||
|
||||
return <MarkdownRender content={markdown} nodes={nodes} />
|
||||
}
|
||||
```
|
||||
|
||||
Code block prop interfaces (`CodeBlockNodeProps`, `MermaidBlockNodeProps`, `D2BlockNodeProps`, `InfographicBlockNodeProps`, `PreCodeNodeProps`) all use `node: CodeBlockNode` from `stream-markdown-parser` (use `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` when targeting specialized renderers).
|
||||
|
||||
## Next.js Best Practices
|
||||
|
||||
Use the dedicated Next SSR entrypoints instead of `mounted` guards or `ssr: false`.
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello Next.js!" final />
|
||||
}
|
||||
```
|
||||
|
||||
For a pure server render path:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/server'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello!" final />
|
||||
}
|
||||
```
|
||||
|
||||
See [React Next SSR](/guide/react-next-ssr) for the full entrypoint model.
|
||||
|
||||
## Hooks Integration
|
||||
|
||||
You can easily integrate with React hooks:
|
||||
|
||||
```tsx
|
||||
import type { ChangeEvent } from 'react'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
function MarkdownEditor() {
|
||||
const [content, setContent] = useState('# Edit me!')
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
const memoizedContent = useMemo(() => content, [content])
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea value={content} onChange={handleChange} />
|
||||
<MarkdownRender
|
||||
content={memoizedContent}
|
||||
customId={`editor-${theme}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function SafeMarkdown({ content }: { content: string }) {
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
Error rendering markdown:
|
||||
{error.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return <MarkdownRender content={content} />
|
||||
}
|
||||
catch (err) {
|
||||
setError(err as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [React Quick Start](/guide/react-quick-start) for setup examples
|
||||
- Explore [Vue 3 Components](/guide/components) for more component examples (API is similar)
|
||||
- Check [Usage & API](/guide/usage) for advanced patterns
|
||||
@@ -0,0 +1,268 @@
|
||||
# React Installation
|
||||
|
||||
Install markstream-react with pnpm, npm or yarn.
|
||||
|
||||
::: tip Already on react-markdown?
|
||||
See the [migration guide](/guide/react-markdown-migration) for the high-level path and the [migration cookbook](/guide/react-markdown-migration-cookbook) for focused before/after examples.
|
||||
:::
|
||||
|
||||
```bash
|
||||
pnpm add markstream-react
|
||||
# or
|
||||
npm install markstream-react
|
||||
# or
|
||||
yarn add markstream-react
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
markstream-react requires:
|
||||
- **React 18+** and **ReactDOM 18+**
|
||||
- **stream-markdown-parser** (auto-installed as dependency)
|
||||
|
||||
## Optional Peer Dependencies
|
||||
|
||||
markstream-react supports various features through optional peer dependencies. Install only what you need:
|
||||
|
||||
| Feature | Required Packages | Install Command |
|
||||
|---------|------------------|-----------------|
|
||||
| Shiki code blocks (`MarkdownCodeBlockNode`) | `stream-markdown` | `pnpm add stream-markdown` |
|
||||
| Monaco Editor (full code block features) | `stream-monaco` | `pnpm add stream-monaco` |
|
||||
| Mermaid Diagrams | `mermaid` | `pnpm add mermaid` |
|
||||
| D2 Diagrams | `@terrastruct/d2` | `pnpm add @terrastruct/d2` |
|
||||
| Math Rendering (KaTeX) | `katex` | `pnpm add katex` |
|
||||
|
||||
## Optional: off-thread workers (Mermaid / KaTeX)
|
||||
|
||||
Mermaid/KaTeX auto-load when installed. If you want off‑thread parsing/rendering, inject the workers:
|
||||
|
||||
```tsx
|
||||
import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
|
||||
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
|
||||
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'
|
||||
|
||||
setMermaidWorker(new MermaidWorker())
|
||||
setKaTeXWorker(new KatexWorker())
|
||||
```
|
||||
|
||||
Required CSS:
|
||||
|
||||
```tsx
|
||||
import 'markstream-react/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
Monaco (`stream-monaco`) does not require a separate CSS import.
|
||||
|
||||
Note: `markstream-react/index.css` is scoped under an internal `.markstream-react` container to reduce global style conflicts. `MarkdownRender` renders inside that container by default. If you render node components standalone, wrap them with `<div className="markstream-react">...</div>`.
|
||||
|
||||
## Tailwind CSS Support
|
||||
|
||||
If your app uses Tailwind and you want to avoid duplicate utility CSS, import the Tailwind-ready output instead:
|
||||
|
||||
```tsx
|
||||
import 'markstream-react/index.tailwind.css'
|
||||
```
|
||||
|
||||
And include the extracted class list in your `tailwind.config.js`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
require('markstream-react/tailwind'),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This approach ensures that Tailwind includes all the utility classes used by markstream-react in its purge process, resulting in a smaller final bundle size.
|
||||
|
||||
### Quick Install: All Features
|
||||
|
||||
To enable all features at once:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown stream-monaco mermaid @terrastruct/d2 katex
|
||||
# or
|
||||
npm install stream-markdown stream-monaco mermaid @terrastruct/d2 katex
|
||||
```
|
||||
|
||||
### Feature Details
|
||||
|
||||
#### Code Syntax Highlighting
|
||||
|
||||
Requires `stream-markdown`:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
```
|
||||
|
||||
`stream-markdown` bundles the Shiki runtime used by `MarkdownCodeBlockNode`. To use Shiki inside `MarkdownRender`, override the `code_block` renderer (or render `MarkdownCodeBlockNode` directly).
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
|
||||
|
||||
setCustomComponents({
|
||||
code_block: ({ node, isDark, ctx }: any) => (
|
||||
<MarkdownCodeBlockNode
|
||||
node={node}
|
||||
isDark={isDark}
|
||||
stream={ctx?.codeBlockStream}
|
||||
{...(ctx?.codeBlockProps || {})}
|
||||
/>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
#### Monaco Editor
|
||||
|
||||
For full code block functionality (copy button, font size controls, expand/collapse):
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
```
|
||||
|
||||
Without `stream-monaco`, code blocks will render but interactive buttons may not work.
|
||||
|
||||
#### Mermaid Diagrams
|
||||
|
||||
For rendering Mermaid diagrams:
|
||||
|
||||
```bash
|
||||
pnpm add mermaid
|
||||
```
|
||||
|
||||
#### D2 Diagrams
|
||||
|
||||
For rendering D2 diagrams:
|
||||
|
||||
```bash
|
||||
pnpm add @terrastruct/d2
|
||||
```
|
||||
|
||||
#### KaTeX Math Rendering
|
||||
|
||||
For math formula rendering:
|
||||
|
||||
```bash
|
||||
pnpm add katex
|
||||
```
|
||||
|
||||
Also import the KaTeX CSS in your app entry:
|
||||
|
||||
```tsx
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
## Quick Test
|
||||
|
||||
Import and render a simple markdown string:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
function App() {
|
||||
const md = '# Hello from markstream-react!'
|
||||
|
||||
return <MarkdownRender content={md} />
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
markstream-react is written in TypeScript and includes full type definitions out of the box. No additional configuration is needed:
|
||||
|
||||
```tsx
|
||||
import type { NodeRendererProps } from 'markstream-react'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
const props: NodeRendererProps = {
|
||||
content: '# Hello TypeScript!',
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <MarkdownRender {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## Next.js Integration
|
||||
|
||||
For Next.js, prefer the dedicated SSR entrypoints:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello Next.js!" final />
|
||||
}
|
||||
```
|
||||
|
||||
If you need a pure server render path with stable fallbacks and no client component boundary:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/server'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello Next.js!" final />
|
||||
}
|
||||
```
|
||||
|
||||
See [React Next SSR](/guide/react-next-ssr) for App Router, Pages Router, custom component, and verification details.
|
||||
|
||||
## Vite Integration
|
||||
|
||||
For Vite projects, simply import the component and styles:
|
||||
|
||||
```tsx
|
||||
// src/main.tsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/App.tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const content = `# Hello Vite!
|
||||
|
||||
This is markstream-react working with **Vite**.`
|
||||
|
||||
return <MarkdownRender content={content} />
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
## Webpack Integration
|
||||
|
||||
For projects using Webpack, ensure your configuration handles CSS imports:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Components Setup
|
||||
|
||||
To use custom node components, you'll need to create a custom renderer. See the [Components documentation](/guide/react-components) for details.
|
||||
@@ -0,0 +1,301 @@
|
||||
# react-markdown Migration Cookbook
|
||||
|
||||
This page complements the main [migration guide](/guide/react-markdown-migration) with focused before/after examples.
|
||||
|
||||
::: tip Prefer a runnable example?
|
||||
See the hosted [React migration demo](https://markstream-react.pages.dev/migration-demo) for the same before/after shapes rendered inside the React playground.
|
||||
:::
|
||||
|
||||
Use this page when you already know you want to migrate, and need to answer a specific question such as:
|
||||
|
||||
- How do I replace `components.code`?
|
||||
- What happens to `remark-gfm`?
|
||||
- What do I do with `rehype-raw`?
|
||||
- How do I replace `urlTransform`?
|
||||
- What about `allowedElements`?
|
||||
|
||||
## 1. Plain renderer swap
|
||||
|
||||
```tsx
|
||||
// before
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <Markdown>{markdown}</Markdown>
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// after
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## 2. `components.h1` and `components.a`
|
||||
|
||||
`react-markdown` customizes rendered HTML tags:
|
||||
|
||||
```tsx
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
<Markdown
|
||||
components={{
|
||||
h1({ children }) {
|
||||
return <h1 className="docs-title">{children}</h1>
|
||||
},
|
||||
a({ href, children }) {
|
||||
return <a href={href} target="_blank" rel="noreferrer">{children}</a>
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
```
|
||||
|
||||
`markstream-react` customizes node types:
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps } from 'markstream-react'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
|
||||
function CustomHeading({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
|
||||
const Tag = `h${node.level || 1}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className="docs-title">
|
||||
{node.children?.map((child: any, i: number) =>
|
||||
renderNode && ctx
|
||||
? renderNode(child, `${String(indexKey)}-heading-${i}`, ctx)
|
||||
: null,
|
||||
)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomLink({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
|
||||
return (
|
||||
<a href={node.href} target="_blank" rel="noreferrer">
|
||||
{node.children?.map((child: any, i: number) =>
|
||||
renderNode && ctx
|
||||
? renderNode(child, `${String(indexKey)}-link-${i}`, ctx)
|
||||
: null,
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
setCustomComponents('docs', {
|
||||
heading: CustomHeading,
|
||||
link: CustomLink,
|
||||
})
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## 3. `components.code`
|
||||
|
||||
Many `react-markdown` apps override `code` to add syntax highlighting:
|
||||
|
||||
```tsx
|
||||
<Markdown
|
||||
components={{
|
||||
code({ className, children, ...rest }) {
|
||||
const isBlock = /language-/.test(className || '')
|
||||
|
||||
if (isBlock) {
|
||||
return (
|
||||
<pre className="docs-code">
|
||||
<code className={className} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return <code {...rest}>{children}</code>
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
```
|
||||
|
||||
In `markstream-react`, choose one of these:
|
||||
|
||||
- Keep the default `CodeBlockNode`
|
||||
- Switch `code_block` to `MarkdownCodeBlockNode`
|
||||
- Set `renderCodeBlocksAsPre`
|
||||
|
||||
Example with `MarkdownCodeBlockNode`:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
|
||||
|
||||
setCustomComponents('docs', {
|
||||
code_block: ({ node, isDark, ctx }: any) => (
|
||||
<MarkdownCodeBlockNode
|
||||
node={node}
|
||||
isDark={isDark}
|
||||
stream={ctx?.codeBlockStream}
|
||||
{...(ctx?.codeBlockProps || {})}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## 4. `remark-gfm`
|
||||
|
||||
Many `react-markdown` examples add:
|
||||
|
||||
```tsx
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
|
||||
```
|
||||
|
||||
With `markstream-react`, do not re-add a plugin by default.
|
||||
|
||||
Start by removing it and verify whether your actual content still renders correctly. Tables, task lists, strikethrough, and code fences are already common use cases for the parser. If your app depended on very specific plugin behavior, review the output before calling the migration complete.
|
||||
|
||||
## 5. `rehype-raw`
|
||||
|
||||
Typical `react-markdown` setup:
|
||||
|
||||
```tsx
|
||||
import Markdown from 'react-markdown'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{markdown}</Markdown>
|
||||
```
|
||||
|
||||
In `markstream-react`, there is no public `rehype` stage.
|
||||
|
||||
Recommended migration path:
|
||||
|
||||
- If you only needed standard HTML rendering, verify the built-in HTML behavior.
|
||||
- If you used trusted custom tags such as `<thinking>`, move to `customHtmlTags`.
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps } from 'markstream-react'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
|
||||
function ThinkingNode({ node }: NodeComponentProps<any>) {
|
||||
return <aside className="thinking-box">{node.content}</aside>
|
||||
}
|
||||
|
||||
setCustomComponents('chat', { thinking: ThinkingNode })
|
||||
|
||||
export function Message({ markdown }: { markdown: string }) {
|
||||
return (
|
||||
<MarkdownRender
|
||||
customId="chat"
|
||||
content={markdown}
|
||||
customHtmlTags={['thinking']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 6. `urlTransform`
|
||||
|
||||
Typical `react-markdown` URL rewriting:
|
||||
|
||||
```tsx
|
||||
function rewriteDocsUrl(url: string) {
|
||||
if (url.startsWith('/docs/'))
|
||||
return `https://example.com${url}`
|
||||
return url
|
||||
}
|
||||
|
||||
<Markdown urlTransform={rewriteDocsUrl}>{markdown}</Markdown>
|
||||
```
|
||||
|
||||
In `markstream-react`, split this into two concerns:
|
||||
|
||||
- `parseOptions.validateLink` for allow/deny decisions
|
||||
- custom `link` or `image` renderers for rewriting
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps } from 'markstream-react'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
|
||||
function rewriteDocsUrl(url: string) {
|
||||
if (url.startsWith('/docs/'))
|
||||
return `https://example.com${url}`
|
||||
return url
|
||||
}
|
||||
|
||||
function CustomLink({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
|
||||
const href = rewriteDocsUrl(node.href)
|
||||
|
||||
return (
|
||||
<a href={href}>
|
||||
{node.children?.map((child: any, i: number) =>
|
||||
renderNode && ctx
|
||||
? renderNode(child, `${String(indexKey)}-link-${i}`, ctx)
|
||||
: null,
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
setCustomComponents('docs', { link: CustomLink })
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## 7. `allowedElements`
|
||||
|
||||
Typical `react-markdown` filtering:
|
||||
|
||||
```tsx
|
||||
<Markdown allowedElements={['h1', 'h2', 'p', 'a', 'code', 'pre']}>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
```
|
||||
|
||||
There is no direct equivalent prop in `markstream-react`.
|
||||
|
||||
Recommended rule:
|
||||
|
||||
- If the allowlist is light and non-critical, migrate the renderer first and leave a TODO.
|
||||
- If the allowlist is security-sensitive or user-facing, filter the parsed node tree before rendering.
|
||||
|
||||
That means this is usually a manual migration item, not an automatic 1:1 replacement.
|
||||
|
||||
## 8. Streaming as a second pass
|
||||
|
||||
Once the static migration is stable, you can move to the parser-driven `nodes` flow for chat/SSE/AI rendering:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('chat')
|
||||
const nodes = parseMarkdownToStructure(buffer, md, { final: done })
|
||||
|
||||
export function Message() {
|
||||
return <MarkdownRender nodes={nodes} viewportPriority deferNodesUntilVisible />
|
||||
}
|
||||
```
|
||||
|
||||
This step is optional. Do not force it when the repository only needs static markdown rendering.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Migrate from react-markdown](/guide/react-markdown-migration)
|
||||
- [React Installation](/guide/react-installation)
|
||||
- [React Components & API](/guide/react-components)
|
||||
- [Parser API](/guide/parser-api)
|
||||
@@ -0,0 +1,285 @@
|
||||
---
|
||||
description: Migrate from react-markdown to markstream-react with guidance on renderer replacement, plugin audits, and non-1:1 customization gaps.
|
||||
---
|
||||
|
||||
# Migrate from react-markdown
|
||||
|
||||
If your current React Markdown renderer is `react-markdown`, moving to `markstream-react` is usually a two-step change:
|
||||
|
||||
1. Replace the renderer component.
|
||||
2. Migrate any custom components or plugin logic that was tied to the `remark` / `rehype` pipeline.
|
||||
|
||||
::: warning Not Always 1:1
|
||||
`markstream-react` is not a drop-in replacement for every `react-markdown` setup. The simple cases migrate quickly, but apps with a long `remarkPlugins` / `rehypePlugins` chain should review each customization before switching.
|
||||
:::
|
||||
|
||||
::: tip See a Live Demo
|
||||
Open the [React migration demo](https://markstream-react.pages.dev/migration-demo) to inspect a synthetic before/after example alongside an audit report generated in the style of the migration skill.
|
||||
:::
|
||||
|
||||
## When to stay on `react-markdown`
|
||||
|
||||
- You only render completed, static Markdown.
|
||||
- Your app depends heavily on the unified `remark` / `rehype` plugin ecosystem.
|
||||
- You need HTML allow/deny lists as first-class component props.
|
||||
|
||||
## When `markstream-react` is a better fit
|
||||
|
||||
- You render AI/chat/SSE output while text is still arriving.
|
||||
- You want progressive Mermaid, D2, KaTeX, or Monaco-backed code blocks.
|
||||
- You want viewport-aware heavy-node scheduling for large documents.
|
||||
- You want to move from string-in/string-out rendering to a streaming-friendly AST model.
|
||||
|
||||
## Smallest possible replacement
|
||||
|
||||
```tsx
|
||||
// before
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <Markdown>{markdown}</Markdown>
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// after
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
If that works for your current content, migrate the rest incrementally.
|
||||
|
||||
## Mental model shift
|
||||
|
||||
- `react-markdown` is HTML-tag oriented: custom rendering usually happens through `components={{ h1, a, code }}`.
|
||||
- `markstream-react` is AST-node oriented: custom rendering usually happens through `setCustomComponents(...)` and node types such as `heading`, `link`, `code_block`, or `inline_code`.
|
||||
- `react-markdown` centers the `remark` -> `rehype` pipeline.
|
||||
- `markstream-react` centers a streaming-friendly parser AST plus node renderers.
|
||||
|
||||
This means `components.h1` does not become `components.h1` again. It usually becomes one `heading` renderer that branches on `node.level`.
|
||||
|
||||
## API mapping
|
||||
|
||||
| `react-markdown` | `markstream-react` | Notes |
|
||||
|---|---|---|
|
||||
| `children` | `content` | Pass the Markdown string through `content`. |
|
||||
| `components` | `setCustomComponents(id?, mapping)` | `react-markdown` keys are HTML tags; `markstream-react` keys are node types such as `heading`, `link`, `paragraph`, `image`, `code_block`, `inline_code`. |
|
||||
| `remarkPlugins` | `customMarkdownIt` | Use `markdown-it` plugins instead of `remark` plugins. Many common Markdown features already work without extra plugins. |
|
||||
| `remarkPlugins={[remarkGfm]}` | Often removable | Tables, task checkboxes, strikethrough, code fences, and other common constructs are already supported by the parser. Re-check edge cases before deleting plugin code. |
|
||||
| `rehypePlugins` | No direct equivalent | There is no public `rehype` stage. Use custom node renderers, `customHtmlTags`, `parseOptions`, or post-process `nodes` instead. |
|
||||
| `rehypeRaw` | Usually not needed | HTML-like tags are already parsed. For custom tags, prefer `customHtmlTags={['thinking']}` plus `setCustomComponents`. |
|
||||
| `skipHtml` | No direct prop | HTML nodes render by default, with built-in blocking of dangerous attributes/tags and unsafe URLs in the HTML renderers. If you must remove HTML entirely, prefilter the input or parsed nodes yourself. |
|
||||
| `allowedElements` / `disallowedElements` / `allowElement` | No direct prop | Filter the parsed `nodes` tree yourself, or replace specific node renderers. |
|
||||
| `unwrapDisallowed` | Manual node filtering | Implement this in your node post-processing step if you need it. |
|
||||
| `urlTransform` | `parseOptions.validateLink` + custom `link` renderer | `validateLink` is for allow/deny. If you need URL rewriting, do it in a custom `link` renderer or while post-processing nodes. |
|
||||
|
||||
## Migrating `components`
|
||||
|
||||
`react-markdown` custom renderers are usually written against HTML-tag names:
|
||||
|
||||
```tsx
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
<Markdown
|
||||
components={{
|
||||
h1({ children }) {
|
||||
return <h1 className="docs-heading">{children}</h1>
|
||||
},
|
||||
a({ href, children }) {
|
||||
return <a href={href} target="_blank" rel="noreferrer">{children}</a>
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
```
|
||||
|
||||
The `markstream-react` version is node-based:
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps } from 'markstream-react'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
function CustomHeading({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
|
||||
const Tag = `h${node.level || 1}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className="docs-heading">
|
||||
{node.children?.map((child: any, i: number) =>
|
||||
renderNode && ctx
|
||||
? renderNode(child, `${String(indexKey)}-heading-${i}`, ctx)
|
||||
: null,
|
||||
)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomLink({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
|
||||
return (
|
||||
<a href={node.href} target="_blank" rel="noreferrer">
|
||||
{node.children?.map((child: any, i: number) =>
|
||||
renderNode && ctx
|
||||
? renderNode(child, `${String(indexKey)}-link-${i}`, ctx)
|
||||
: null,
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
setCustomComponents('docs', {
|
||||
heading: CustomHeading,
|
||||
link: CustomLink,
|
||||
})
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
Useful node-type translations:
|
||||
|
||||
- `h1` ... `h6` -> `heading` + `node.level`
|
||||
- `a` -> `link`
|
||||
- `p` -> `paragraph`
|
||||
- `img` -> `image`
|
||||
- `code` / `pre` -> `code_block` or `inline_code`
|
||||
- Custom HTML-like tags -> `customHtmlTags` + `setCustomComponents`
|
||||
|
||||
## Migrating code highlighting
|
||||
|
||||
Many `react-markdown` apps override `components.code` and wire in a third-party highlighter.
|
||||
|
||||
With `markstream-react`, you have three common choices:
|
||||
|
||||
- Keep the default `CodeBlockNode` for Monaco-powered code blocks.
|
||||
- Swap `code_block` to `MarkdownCodeBlockNode` for a lighter Shiki-based renderer.
|
||||
- Set `renderCodeBlocksAsPre` when you want plain `<pre><code>`.
|
||||
|
||||
Example: swap code blocks to Shiki:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
|
||||
|
||||
setCustomComponents('docs', {
|
||||
code_block: ({ node, isDark, ctx }: any) => (
|
||||
<MarkdownCodeBlockNode
|
||||
node={node}
|
||||
isDark={isDark}
|
||||
stream={ctx?.codeBlockStream}
|
||||
{...(ctx?.codeBlockProps || {})}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## Migrating plugin logic
|
||||
|
||||
### `remarkPlugins`
|
||||
|
||||
If you used `remarkPlugins` for syntax extensions, the nearest equivalent is `customMarkdownIt`.
|
||||
|
||||
```tsx
|
||||
import type { MarkdownIt } from 'markdown-it-ts'
|
||||
import { full as markdownItEmoji } from 'markdown-it-emoji'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function withEmoji(md: MarkdownIt) {
|
||||
md.use(markdownItEmoji)
|
||||
return md
|
||||
}
|
||||
|
||||
export function Article({ markdown }: { markdown: string }) {
|
||||
return <MarkdownRender content={markdown} customMarkdownIt={withEmoji} />
|
||||
}
|
||||
```
|
||||
|
||||
### `rehypePlugins`
|
||||
|
||||
There is no direct `rehype` step in `markstream-react`.
|
||||
|
||||
If your old `rehype` logic was used for:
|
||||
|
||||
- HTML tree rewrites: move that logic into custom node renderers or node post-processing.
|
||||
- Custom HTML-like blocks: prefer `customHtmlTags`.
|
||||
- Security allow/deny logic: use `parseOptions.validateLink`, custom renderers, or node filtering.
|
||||
|
||||
## HTML, custom tags, and security
|
||||
|
||||
`react-markdown` usually escapes HTML unless you add something like `rehypeRaw`.
|
||||
|
||||
`markstream-react` already parses HTML-like nodes, and its built-in HTML renderers strip blocked tags, dangerous event-handler attributes, and unsafe URLs.
|
||||
|
||||
If your old `rehypeRaw` usage was mainly there to support custom tags such as `<thinking>`, this is the recommended migration path:
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps } from 'markstream-react'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
|
||||
function ThinkingNode({ node }: NodeComponentProps<any>) {
|
||||
return <aside className="thinking-box">{node.content}</aside>
|
||||
}
|
||||
|
||||
setCustomComponents('chat', { thinking: ThinkingNode })
|
||||
|
||||
export function Message({ markdown }: { markdown: string }) {
|
||||
return (
|
||||
<MarkdownRender
|
||||
customId="chat"
|
||||
content={markdown}
|
||||
customHtmlTags={['thinking']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming upgrade path
|
||||
|
||||
You do not need to adopt streaming on day one. A practical migration path is:
|
||||
|
||||
1. Replace `react-markdown` with `MarkdownRender` and keep using `content`.
|
||||
2. Migrate custom renderers and plugin logic.
|
||||
3. If you later render SSE/chat output, parse outside the component and pass `nodes`.
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('chat')
|
||||
const nodes = parseMarkdownToStructure(buffer, md, { final: done })
|
||||
|
||||
export function Message() {
|
||||
return (
|
||||
<MarkdownRender
|
||||
nodes={nodes}
|
||||
viewportPriority
|
||||
deferNodesUntilVisible
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For high-frequency streaming, this `nodes` flow is usually the biggest reason to migrate.
|
||||
|
||||
## Migration checklist
|
||||
|
||||
- Replace `<Markdown>{markdown}</Markdown>` with `<MarkdownRender content={markdown} />`.
|
||||
- Import `markstream-react/index.css`.
|
||||
- Move `components` overrides into `setCustomComponents(customId, mapping)`.
|
||||
- Remove plugins that are now redundant before re-adding custom ones.
|
||||
- Re-check any `rehype`-based HTML filtering or transformation logic.
|
||||
- If your app renders incremental output, move from `content` to parsed `nodes`.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [React Installation](/guide/react-installation)
|
||||
- [React Components & API](/guide/react-components)
|
||||
- [Parser API](/guide/parser-api)
|
||||
@@ -0,0 +1,83 @@
|
||||
# React Next SSR
|
||||
|
||||
`markstream-react` now exposes two explicit SSR entrypoints for Next.js:
|
||||
|
||||
- `markstream-react/next`: the official Next entry. Import it from App Router server components or Pages Router SSR pages when you want real server HTML first and client enhancement after hydration.
|
||||
- `markstream-react/server`: a pure server entry. Use it when you want a stable server-only render path with no client component boundary.
|
||||
|
||||
Import styles once from your app shell:
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx or pages/_app.tsx
|
||||
import 'markstream-react/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
## App Router with `markstream-react/next`
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
|
||||
const markdown = `# Hello Next.js
|
||||
|
||||
This route ships real SSR HTML first.`
|
||||
|
||||
export default function Page() {
|
||||
return <MarkdownRender content={markdown} final />
|
||||
}
|
||||
```
|
||||
|
||||
## Pure Server Rendering with `markstream-react/server`
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/server'
|
||||
|
||||
const markdown = `# Server entry
|
||||
|
||||
This path stays fully server-rendered.`
|
||||
|
||||
export default function Page() {
|
||||
return <MarkdownRender content={markdown} final />
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Components and Custom Tags
|
||||
|
||||
If you register overrides from a server file, prefer the server helpers so you avoid importing the root package side effects:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react/server'
|
||||
|
||||
setCustomComponents('next-ssr-demo', {
|
||||
thinking: ({ children }: any) => <aside data-ssr-status="thinking-tag">{children}</aside>,
|
||||
})
|
||||
```
|
||||
|
||||
Then render with `customId="next-ssr-demo"` and `customHtmlTags={['thinking']}`.
|
||||
|
||||
Current SSR acceptance also covers:
|
||||
|
||||
- `customId` scoping isolation
|
||||
- custom node children rendered into the initial HTML
|
||||
- `customHtmlTags` attributes preserved in the initial HTML
|
||||
|
||||
## TypeScript
|
||||
|
||||
`markstream-react/next` and `markstream-react/server` both publish standalone declaration files. You can import shared prop types directly from the entrypoint you use:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
HtmlPreviewFrameProps,
|
||||
MarkdownCodeBlockNodeProps,
|
||||
NodeRendererProps,
|
||||
TooltipProps,
|
||||
} from 'markstream-react/next'
|
||||
```
|
||||
|
||||
## Current Verification Commands
|
||||
|
||||
```bash
|
||||
pnpm exec vitest run test/markstream-react-next-ssr.test.tsx
|
||||
pnpm --filter markstream-react build
|
||||
pnpm run test:e2e:next-ssr
|
||||
```
|
||||
@@ -0,0 +1,452 @@
|
||||
# React Quick Start
|
||||
|
||||
Get started with markstream-react in your React project.
|
||||
|
||||
::: tip Migrating from react-markdown?
|
||||
If your app already uses `react-markdown`, start with the [migration guide](/guide/react-markdown-migration) and the [migration cookbook](/guide/react-markdown-migration-cookbook) before wiring new code by hand.
|
||||
:::
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Installation
|
||||
|
||||
First, install the package:
|
||||
|
||||
```bash
|
||||
pnpm add markstream-react
|
||||
```
|
||||
|
||||
### 2. Import Styles
|
||||
|
||||
In your main entry file (e.g., `main.tsx`, `index.tsx`, or `App.tsx`):
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Use the Component
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const markdown = `# Hello React!
|
||||
|
||||
This is **markstream-react** - a streaming-friendly Markdown renderer for React.
|
||||
|
||||
## Features
|
||||
|
||||
- Code syntax highlighting
|
||||
- Mermaid diagrams
|
||||
- Math formulas
|
||||
- And much more!
|
||||
|
||||
\`\`\`javascript
|
||||
console.log('Hello from React!')
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MarkdownRender content={markdown} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
## Using with TypeScript
|
||||
|
||||
markstream-react is built with TypeScript and includes full type definitions:
|
||||
|
||||
```tsx
|
||||
import type { NodeRendererProps } from 'markstream-react'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
const props: NodeRendererProps = {
|
||||
content: '# Hello TypeScript!',
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <MarkdownRender {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## Using with Next.js
|
||||
|
||||
Use the dedicated SSR entrypoints instead of `mounted` guards or `ssr: false`.
|
||||
|
||||
See [React Next SSR](/guide/react-next-ssr) for the full contract and examples.
|
||||
|
||||
### App Router (Next.js 14 / 15)
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
const markdown = `# Hello Next.js!
|
||||
|
||||
This route ships real SSR HTML first.
|
||||
`
|
||||
|
||||
return <MarkdownRender content={markdown} final />
|
||||
}
|
||||
```
|
||||
|
||||
### Pages Router
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
const markdown = `# Hello Next.js Pages Router!`
|
||||
|
||||
return <MarkdownRender content={markdown} final />
|
||||
}
|
||||
```
|
||||
|
||||
### Pure Server Rendering
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/server'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
const markdown = `# Pure server entry`
|
||||
|
||||
return <MarkdownRender content={markdown} final />
|
||||
}
|
||||
```
|
||||
|
||||
## Enabling Optional Features
|
||||
|
||||
### Code Syntax Highlighting
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
```
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
|
||||
|
||||
// Use Shiki-based code blocks inside MarkdownRender
|
||||
setCustomComponents({
|
||||
code_block: ({ node, isDark, ctx }: any) => (
|
||||
<MarkdownCodeBlockNode
|
||||
node={node}
|
||||
isDark={isDark}
|
||||
stream={ctx?.codeBlockStream}
|
||||
{...(ctx?.codeBlockProps || {})}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
function App() {
|
||||
const markdown = `\`\`\`javascript
|
||||
const hello = 'world'
|
||||
console.log(hello)
|
||||
\`\`\``
|
||||
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
### Mermaid Diagrams
|
||||
|
||||
Install mermaid:
|
||||
|
||||
```bash
|
||||
pnpm add mermaid
|
||||
```
|
||||
|
||||
Import styles (Mermaid auto-loads when installed):
|
||||
|
||||
```tsx
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
function App() {
|
||||
const markdown = `#### Mermaid Diagram
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Is it working?}
|
||||
B -->|Yes| C[Great!]
|
||||
B -->|No| D[Keep trying]
|
||||
\`\`\``
|
||||
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
### D2 Diagrams
|
||||
|
||||
Install D2:
|
||||
|
||||
```bash
|
||||
pnpm add @terrastruct/d2
|
||||
```
|
||||
|
||||
```tsx
|
||||
import 'markstream-react/index.css'
|
||||
|
||||
function App() {
|
||||
const markdown = `#### D2 Diagram
|
||||
|
||||
\`\`\`d2
|
||||
direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query
|
||||
DB -> API: rows
|
||||
API -> Client: response
|
||||
\`\`\``
|
||||
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
### Math Formulas (KaTeX)
|
||||
|
||||
Install katex:
|
||||
|
||||
```bash
|
||||
pnpm add katex
|
||||
```
|
||||
|
||||
Import styles (KaTeX auto-loads when installed):
|
||||
|
||||
```tsx
|
||||
import 'markstream-react/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
function App() {
|
||||
const markdown = `#### Math Example
|
||||
|
||||
Inline math: $E = mc^2$
|
||||
|
||||
Block math:
|
||||
|
||||
$$
|
||||
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
|
||||
$$`
|
||||
|
||||
return <MarkdownRender content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Components
|
||||
|
||||
You can customize how specific nodes are rendered by passing custom component mappings:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
|
||||
// Custom heading component
|
||||
function CustomHeading({ node, customId }: any) {
|
||||
const level = node.level || 1
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className="custom-heading" data-custom-id={customId}>
|
||||
{node.children?.map((child: any, i: number) => (
|
||||
<span key={i}>{child.content || ''}</span>
|
||||
))}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
// Register for a scoped customId
|
||||
setCustomComponents('docs', { heading: CustomHeading })
|
||||
|
||||
function App() {
|
||||
const markdown = `# Custom Heading
|
||||
|
||||
This heading is rendered with a custom component.
|
||||
`
|
||||
|
||||
return <MarkdownRender customId="docs" content={markdown} />
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming Content
|
||||
|
||||
markstream-react supports streaming markdown content, which is useful for AI-generated content:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function StreamingDemo() {
|
||||
const [markdown, setMarkdown] = useState('')
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
|
||||
const fullText = `# Streaming Demo
|
||||
|
||||
This content is being streamed **in small chunks**.
|
||||
|
||||
## Features
|
||||
|
||||
1. Progressive rendering
|
||||
2. No layout shift
|
||||
3. Smooth animations
|
||||
|
||||
\`\`\`javascript
|
||||
const streaming = true
|
||||
console.log('Streaming enabled:', streaming)
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
const startStreaming = () => {
|
||||
setIsStreaming(true)
|
||||
setMarkdown('')
|
||||
let i = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (i < fullText.length) {
|
||||
setMarkdown(prev => prev + fullText[i])
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
setIsStreaming(false)
|
||||
}
|
||||
}, 20)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={startStreaming} disabled={isStreaming}>
|
||||
{isStreaming ? 'Streaming...' : 'Start Streaming'}
|
||||
</button>
|
||||
<MarkdownRender content={markdown} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Using with React Hooks
|
||||
|
||||
```tsx
|
||||
import type { ChangeEvent } from 'react'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
function MarkdownEditor() {
|
||||
const [content, setContent] = useState('# Edit me!')
|
||||
const [html, setHtml] = useState('')
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="markdown-editor">
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={handleChange}
|
||||
placeholder="Write your markdown here..."
|
||||
className="editor-input"
|
||||
/>
|
||||
<div className="editor-preview">
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Virtualized Lists
|
||||
|
||||
For large markdown documents, you can use virtualization:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function LongDocument() {
|
||||
// Your very long markdown content
|
||||
const markdown = `# Long Document`
|
||||
|
||||
return (
|
||||
<MarkdownRender
|
||||
content={markdown}
|
||||
maxLiveNodes={200}
|
||||
liveNodeBuffer={10}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Common Props
|
||||
|
||||
Below are the most commonly used props. For the full list, see [React Components](/guide/react-components).
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `string` | - | Markdown content to render |
|
||||
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]`) |
|
||||
| `customId` | `string` | - | Identifier for scoping |
|
||||
| `maxLiveNodes` | `number` | `320` | Max nodes for virtualization |
|
||||
| `liveNodeBuffer` | `number` | `60` | Buffer for overscan |
|
||||
| `batchRendering` | `boolean` | `true` | Enable batched rendering |
|
||||
| `deferNodesUntilVisible` | `boolean` | `true` | Defer heavy nodes |
|
||||
| `renderCodeBlocksAsPre` | `boolean` | `false` | Use `<pre><code>` fallback (Mermaid blocks will also fall back) |
|
||||
|
||||
## Styling
|
||||
|
||||
The default styles are scoped under `.markstream-react` class. You can override styles:
|
||||
|
||||
```css
|
||||
/* Your global styles */
|
||||
.markstream-react {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markstream-react h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markstream-react code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Using with Tailwind CSS
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import 'markstream-react/index.css'
|
||||
import './output.css' // Your Tailwind output
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8">
|
||||
<MarkdownRender content="# Hello Tailwind!" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore [React Components documentation](/guide/react-components) for all available components
|
||||
- Check out [Examples](/guide/examples) for more usage examples
|
||||
- See [API Reference](/guide/components) for detailed API documentation
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
description: Fix Tailwind, shadcn, and CSS ordering issues when importing markstream-vue styles into a controlled layer.
|
||||
---
|
||||
|
||||
# Tailwind Integration & Style Ordering
|
||||
|
||||
If your project uses a Tailwind component library like `shadcn`, you may run into style ordering or overriding issues when including `markstream-vue` CSS. The recommended approach is to import the library CSS inside a controlled Tailwind layer.
|
||||
|
||||
`markstream-vue` scopes its packaged CSS under an internal `.markstream-vue` container to reduce global conflicts. Tailwind utilities from `markstream-vue/index.css` apply only inside the renderer container.
|
||||
|
||||
Example `styles/index.css`:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
Alternatives:
|
||||
- Place the library CSS inside `@layer base` if you want it to be more foundational and harder to override.
|
||||
- Import the library CSS after your component or application CSS if you prefer the library styles to win.
|
||||
|
||||
Always re-run your dev server after changing the import order.
|
||||
|
||||
## Avoid duplicate CSS with Tailwind
|
||||
|
||||
To reduce duplicated utility CSS when using Tailwind, prefer the "tailwind-ready" output shipped by this package instead of the full, precompiled `index.css`.
|
||||
|
||||
- Tailwind v3 (recommended workflow):
|
||||
- Import `index.tailwind.css` in your app so consumers get only the framework+component styles without embedded Tailwind utilities.
|
||||
- Add the generated class list to your `tailwind.config.js` `content` so Tailwind can pick up the classes used by the renderer and not re-generate them.
|
||||
|
||||
Example (Tailwind v3 / `tailwind.config.js`):
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,ts,vue}',
|
||||
// include the helper produced by the package
|
||||
// installed packages can reference: require('markstream-vue/tailwind')
|
||||
require('markstream-vue/tailwind'),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Example CSS import (app entry):
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import 'markstream-vue/index.tailwind.css';
|
||||
```
|
||||
|
||||
- Tailwind v4: you can directly include `index.tailwind.css` and rely on the v4 scanner to discover classes without needing the extra `tailwind.ts` helper.
|
||||
|
||||
- Non-Tailwind projects: continue to import the precompiled `index.css`:
|
||||
|
||||
```css
|
||||
@import 'markstream-vue/index.css';
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The package exposes a `./tailwind` entry (`./dist/tailwind.ts`) which exports the extracted class list. When installing from npm, `require('markstream-vue/tailwind')` will load that helper for use in your Tailwind config.
|
||||
- If you develop locally against the repo, you may reference the generated file directly (e.g. `./dist/tailwind.ts`).
|
||||
|
||||
Quick try — verify Tailwind classes are discovered by adding `index.tailwind.css` and a small component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Tailwind test\n\nThis text is styled by tailwind'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose">
|
||||
<MarkdownRender :content="md" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## UnoCSS integration
|
||||
|
||||
UnoCSS follows the same rules: import resets first, then wrap renderer CSS so component styles stay in the correct layer.
|
||||
|
||||
1. Install Uno presets (`presetUno`, `presetWind`) and include the package CSS inside a `preflight`:
|
||||
|
||||
```ts
|
||||
// uno.config.ts
|
||||
import { defineConfig, presetUno, presetWind } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [presetUno(), presetWind()],
|
||||
preflights: [
|
||||
{
|
||||
// ensures the CSS runs with the base reset
|
||||
layer: 'preflights',
|
||||
getCSS: () => '@import "markstream-vue/index.css";',
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
2. If you prefer to rely on CSS layers manually, include the CSS in your main stylesheet:
|
||||
|
||||
```css
|
||||
@import '@unocss/reset/tailwind.css';
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
3. Use Uno prefixes (e.g., `uno` config `rules: [], shortcuts: [], theme: {}` + `shortcutsPrefix: 'u-'`) when class names collide with renderer classes. Tailwind users can also set `prefix: 'tw-'` in `tailwind.config.js`.
|
||||
|
||||
Troubleshooting tip: enable the “Cascade Layers” view in devtools to check whether Uno/Tailwind utilities override renderer selectors. If utilities appear below the component layer, move the import into `@layer components` or adjust the preflight order.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Thanks
|
||||
|
||||
This project is built with help from several open source projects and libraries. Big thanks to the authors and maintainers of:
|
||||
|
||||
- [stream-monaco](https://github.com/Simon-He95/stream-monaco)
|
||||
- [stream-markdown](https://github.com/Simon-He95/stream-markdown)
|
||||
- [mermaid](https://mermaid-js.github.io/mermaid)
|
||||
- [shiki](https://github.com/shikijs/shiki)
|
||||
- [markdown-it-ts](https://github.com/Simon-He95/markdown-it-ts)
|
||||
|
||||
If you want to support the project, star it on GitHub or open an issue to request a feature.
|
||||
|
||||
## Star History
|
||||
|
||||
An interactive Star History graph is available (live on GitHub):
|
||||
|
||||
[](https://www.star-history.com/#Simon-He95/markstream-vue&Date)
|
||||
|
||||
Try this — run the playground locally to test examples and say thanks by starring the repo:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm play
|
||||
# open the playground at the shown dev server and try interactive demos
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# Translation Guide
|
||||
|
||||
Thanks for helping translate the docs — contributions are highly appreciated! This guide describes the recommended workflow for translating English docs to other languages (we currently offer `zh` Chinese translations).
|
||||
|
||||
## Basic workflow
|
||||
|
||||
1. Copy an English page from `docs/guide/` into `docs/zh/guide/` and keep the same filename.
|
||||
2. Translate the content while preserving code blocks, front-matter (if present), and internal links (use `/zh/` prefix for links to other zh pages).
|
||||
3. Run local docs dev or build to test:
|
||||
|
||||
```bash
|
||||
pnpm docs:dev
|
||||
# or
|
||||
pnpm docs:build
|
||||
```
|
||||
|
||||
4. Open a PR. Tag it with `docs` and optionally `i18n`.
|
||||
|
||||
## Link handling
|
||||
|
||||
- Update internal docs links to `zh` variants when pointing to translated pages. For example:
|
||||
|
||||
- English: `[Quick Start](/guide/quick-start)`
|
||||
- Chinese: `[快速开始](/zh/guide/quick-start)`
|
||||
|
||||
- Keep external links unchanged (unchanged URLs or GitHub links).
|
||||
|
||||
## Style & tone
|
||||
|
||||
- Keep technical terms — prefer consistent translations for technical terms (`Monaco`, `Mermaid`, `KaTeX`, etc.). If unsure, keep the English term and add explanatory parentheses.
|
||||
- For code examples, do not change syntax; only translate surrounding text.
|
||||
|
||||
## Check the site locally
|
||||
|
||||
- Run `pnpm docs:dev` and choose the language at the top-right language selector.
|
||||
- Check navigation and whether the sidebar shows the translated pages.
|
||||
|
||||
## Checklist for PR authors
|
||||
|
||||
- [ ] Pages placed under `docs/zh/guide/` with matching filenames
|
||||
- [ ] Links to other docs updated to `zh` version where appropriate
|
||||
- [ ] `pnpm docs:build` completes with no errors
|
||||
- [ ] Add a short PR description noting language and scope of changes
|
||||
|
||||
If you'd like me to add a GitHub Action that automatically checks PRs for missing translations or broken links, I can add that as a follow-up. Thank you for contributing translations!
|
||||
|
||||
Try this — validate your local docs translation build:
|
||||
|
||||
```bash
|
||||
pnpm docs:dev
|
||||
# switch to /zh/ using the language selector and confirm pages render
|
||||
```
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
description: Diagnose markstream-vue problems by symptom across CSS order, missing peers, SSR boundaries, trusted tags, and streaming performance.
|
||||
---
|
||||
|
||||
# Troubleshooting by Symptom
|
||||
|
||||
Use this page when something is wrong but you are not yet sure whether the cause is CSS order, missing peers, SSR boundaries, custom-tag wiring, or renderer choice.
|
||||
|
||||
If you already know the exact subsystem, jump straight to the deeper page linked from each section below.
|
||||
|
||||
## Start with the symptom you see
|
||||
|
||||
| What you see | Start here | Then go to |
|
||||
| --- | --- | --- |
|
||||
| Styles look broken, spacing is off, or Tailwind wins | [CSS looks wrong? Start here](/guide/troubleshooting#css-looks-wrong-start-here) | [Tailwind Integration & Style Ordering](/guide/tailwind) |
|
||||
| A trusted tag like `<thinking>` renders as raw HTML | [Docs Site & VitePress](/guide/vitepress-docs-integration) or [Custom Tags & Advanced Components](/guide/custom-components) | [API Reference](/guide/api) |
|
||||
| `window is not defined` or browser-only peers crash during SSR | [Troubleshooting](/guide/troubleshooting) | [Nuxt SSR](/nuxt-ssr) |
|
||||
| Mermaid, KaTeX, Monaco, or D2 does not render | [Installation](/guide/installation) | [Renderer & Node Components](/guide/components) |
|
||||
| Chat output feels slow or reparses too much | [AI Chat & Streaming](/guide/ai-chat-streaming) | [Performance](/guide/performance) |
|
||||
| A built-in node is the wrong shape for your app | [Override Built-in Components](/guide/component-overrides) | [Renderer & Node Components](/guide/components) |
|
||||
|
||||
## 1. Styles look wrong
|
||||
|
||||
Symptoms:
|
||||
|
||||
- paragraphs, tables, or lists have odd spacing
|
||||
- code blocks or images look unstyled
|
||||
- utility classes from Tailwind or UnoCSS override the renderer
|
||||
|
||||
Check in this order:
|
||||
|
||||
1. import your reset before `markstream-vue/index.css`
|
||||
2. if you use Tailwind or UnoCSS, wrap the import in `@layer components`
|
||||
3. if math is enabled, import `katex/dist/katex.min.css`
|
||||
4. if the renderer lives in a larger app surface, scope overrides with `custom-id`
|
||||
|
||||
Start with the CSS checklist: [Troubleshooting](/guide/troubleshooting#css-looks-wrong-start-here)
|
||||
|
||||
## 2. Custom tags or custom components do not appear
|
||||
|
||||
Symptoms:
|
||||
|
||||
- `<thinking>` shows up as raw HTML
|
||||
- your custom Vue component never renders
|
||||
- the component works in one place but not another
|
||||
|
||||
Check in this order:
|
||||
|
||||
1. make sure the tag is listed in `custom-html-tags`
|
||||
2. register the mapping with `setCustomComponents(customId, mapping)`
|
||||
3. render the page with the matching `custom-id`
|
||||
4. if you are in VitePress, put the registration in `enhanceApp`
|
||||
|
||||
Best paths:
|
||||
|
||||
- docs-site or theme case: [Docs Site & VitePress](/guide/vitepress-docs-integration)
|
||||
- app-level custom tags: [Custom Tags & Advanced Components](/guide/custom-components)
|
||||
|
||||
## 3. SSR errors or browser-only crashes
|
||||
|
||||
Symptoms:
|
||||
|
||||
- `window is not defined`
|
||||
- Mermaid or Monaco works locally but fails in SSR
|
||||
- hydration differs between server and client
|
||||
|
||||
Check in this order:
|
||||
|
||||
1. confirm whether the failing peer is browser-only
|
||||
2. move that initialization behind client-only boundaries
|
||||
3. keep server-safe rendering on the base `MarkdownRender` path
|
||||
|
||||
Best paths:
|
||||
|
||||
- general checklist: [Troubleshooting](/guide/troubleshooting)
|
||||
- Nuxt-specific rules: [Nuxt SSR](/nuxt-ssr)
|
||||
|
||||
## 4. Heavy features do not render
|
||||
|
||||
Symptoms:
|
||||
|
||||
- Mermaid fences stay as source text
|
||||
- formulas stay blank
|
||||
- Monaco blocks are empty
|
||||
- D2 falls back to source
|
||||
|
||||
Most of the time, this is not a parser bug. It is one of:
|
||||
|
||||
- the peer package is not installed
|
||||
- required CSS is missing, especially for KaTeX
|
||||
- SSR or worker boundaries are wrong
|
||||
|
||||
Start with [Installation](/guide/installation), then confirm the component-specific notes in [Renderer & Node Components](/guide/components).
|
||||
|
||||
## 5. Streaming or chat output feels slow
|
||||
|
||||
Symptoms:
|
||||
|
||||
- every token update causes visible reflow
|
||||
- long chat transcripts get heavy quickly
|
||||
- the app reparses too much Markdown on each update
|
||||
|
||||
The usual fix is architectural rather than cosmetic:
|
||||
|
||||
- parse outside `MarkdownRender`
|
||||
- pass `nodes` + `final`
|
||||
- keep heavy peers off until they are needed
|
||||
|
||||
Best path: [AI Chat & Streaming](/guide/ai-chat-streaming), then [Performance](/guide/performance)
|
||||
|
||||
## 6. Still not sure?
|
||||
|
||||
If you still cannot classify the issue:
|
||||
|
||||
1. reproduce it in the playground with the smallest Markdown sample possible
|
||||
2. remove optional peers until the failure becomes simpler
|
||||
3. compare against the nearest scenario guide:
|
||||
`Docs Site & VitePress`, `AI Chat & Streaming`, or `Usage & Streaming`
|
||||
|
||||
If it still looks like a real bug, collect:
|
||||
|
||||
- the minimal Markdown sample
|
||||
- framework/runtime details
|
||||
- whether Tailwind, UnoCSS, or SSR is involved
|
||||
- which optional peers are installed
|
||||
|
||||
Then use the hosted test page or issue link from [Troubleshooting](/guide/troubleshooting).
|
||||
@@ -0,0 +1,107 @@
|
||||
# Troubleshooting
|
||||
|
||||
If you do not yet know whether the issue is CSS, peers, SSR, or custom-tag wiring, start with [Troubleshooting by Symptom](/guide/troubleshooting-path) first.
|
||||
|
||||
If something breaks, here are common fixes:
|
||||
|
||||
- If you get `window is not defined` during SSR, wrap components in `<client-only>` for Nuxt and use the `onMounted` guard for Vite SSR.
|
||||
- Install `katex` when math rendering fails, and import `katex/dist/katex.min.css` in your app entry.
|
||||
- For Mermaid issues, upgrade or pin `mermaid` to a version >= 11 and check asynchronous render logs.
|
||||
- If you load KaTeX/Mermaid via CDN `<script>`, make sure `window.katex` / `window.mermaid` exist before first render, or call `setKatexLoader(() => window.katex)` / `setMermaidLoader(() => window.mermaid)` once after they load.
|
||||
- For performance concerns, check that `viewportPriority` is true and avoid too many heavy nodes in a single mount.
|
||||
|
||||
If unsure, reproduce issue using the playground (or the hosted quick test) and then open an issue with stack trace and minimal markdown sample.
|
||||
|
||||
Hosted quick test: https://markstream-vue.simonhe.me/test
|
||||
|
||||
Open a new issue (quick link): https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
|
||||
|
||||
## Common problems (FAQ)
|
||||
|
||||
- Tailwind / CSS utilities overriding styling: If your project uses Tailwind or a component library (e.g., shadcn), utility classes or global styles may override the library's styles. See the Tailwind integration guide for recommended import ordering and strategies: [/guide/tailwind](/guide/tailwind).
|
||||
|
||||
Quick fixes:
|
||||
|
||||
- Import `markstream-vue/index.css` inside a `@layer components { ... }` block (see the Tailwind page). This controls style ordering.
|
||||
- Consider setting a `prefix` in Tailwind config (e.g., `tw-`) to avoid collisions with component library class names.
|
||||
- Use scoped selectors or `:deep` to target only the elements you need to override.
|
||||
|
||||
Note: `markstream-vue` scopes its packaged CSS under an internal `.markstream-vue` container (including theme variables and Tailwind utilities), so most conflicts come from ordering/resets inside the renderer area rather than global leakage.
|
||||
|
||||
- Want to tweak looks? You can customize via CSS variables in `src/index.css` (e.g., `--vscode-editor-background`, `--vscode-editor-foreground`) or override component classes in your app's stylesheet. Use the `@apply` helper or your own stylesheet to keep style rules isolated.
|
||||
|
||||
- Slots first: if the styling you want needs layout changes inside a component, check whether the component provides a named slot (e.g., `header-left`, `header-right`, `loading`). Slots are an easy and stable escape hatch that let you re-render interior markup without overriding internals.
|
||||
|
||||
- Still not enough? Use `setCustomComponents(id, mapping)` to replace an entire renderer for a node with your own Vue component. See the `Advanced` guide for `setCustomComponents` usage and how to scope them to a `custom-id`.
|
||||
Quick example (replace the renderer for `code_block` nodes scoped to `my-docs`):
|
||||
|
||||
```ts
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
setCustomComponents('my-docs', {
|
||||
code_block: MyCustomCodeBlock,
|
||||
})
|
||||
```
|
||||
|
||||
This will cause all `MarkdownRender` instances with `custom-id="my-docs"` to render the `code_block` node using `MyCustomCodeBlock` instead of the default. See `Advanced` for more examples.
|
||||
|
||||
- Repro / report flow: When you run into rendering anomalies or errors, try to reproduce them with the `playground` app and a minimal markdown sample. If it's still broken, run unit tests locally with `pnpm test` to rule out regressions. Then open an issue with:
|
||||
|
||||
1. Minimal markdown example that reproduces the problem (include in the issue body or link to a gist).
|
||||
2. Steps to reproduce (playground link or `playground` instructions), and the environment (browser, Node version, Vite/Nuxt).
|
||||
3. Any error stack traces and relevant console output.
|
||||
|
||||
Prefer to share a small `playground` reproduction and include a link; maintainers will triage and ask for more context if needed.
|
||||
|
||||
Extra tip: if you can write a unit/integration test that reproduces the bug, add it under the `test/` folder and run `pnpm test` locally — this is the fastest way for maintainers to validate and fix regressions.
|
||||
|
||||
## CSS looks wrong? Start here
|
||||
|
||||
Most rendering bugs are caused by style ordering, missing resets, or utility frameworks (Tailwind/UnoCSS) trumping component CSS. Run through this checklist before filing an issue:
|
||||
|
||||
1. **Reset browser defaults** — margins on `p`, `dl`, `table`, and `pre` differ per browser. Import a reset (`modern-css-reset`, `@unocss/reset`, or Tailwind's `@tailwind base`) *before* `markstream-vue` styles:
|
||||
|
||||
```css
|
||||
@import 'modern-css-reset';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@import 'markstream-vue/index.css';
|
||||
```
|
||||
|
||||
2. **Use CSS layers** — When Tailwind or UnoCSS runs in `@layer components` or `utilities`, wrap the library CSS import so the cascade is predictable:
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
For UnoCSS, inject the CSS inside `preflights`:
|
||||
|
||||
```ts
|
||||
import { defineConfig, presetUno } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [presetUno()],
|
||||
preflights: [
|
||||
{
|
||||
layer: 'components',
|
||||
getCSS: () => '@import "markstream-vue/index.css";',
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
3. **Double-check peer CSS** — KaTeX ships CSS. Missing imports show up as invisible formulas. Mermaid does not require extra CSS.
|
||||
|
||||
4. **Scope overrides via `custom-id`** — When integrating with larger design systems, renderers can stomp on each other. Add `custom-id="docs"` to `MarkdownRender` and apply overrides scoped to `[data-custom-id="docs"]` so other layouts stay untouched.
|
||||
|
||||
If none of the above helps, reproduce the issue in the playground (`pnpm play`) with a CSS-only snippet and include the link when reporting the bug.
|
||||
|
||||
Quick test — run the playground locally to reproduce and debug:
|
||||
|
||||
```bash
|
||||
pnpm play
|
||||
# open the playground and reproduce the issue with a minimal Markdown sample
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# TypeScript Usage
|
||||
|
||||
This library is TypeScript-first and exports types for the public API. Use `import type` to get node definitions in your code for advanced manipulations.
|
||||
|
||||
```ts twoslash
|
||||
import type { BaseNode } from 'markstream-vue'
|
||||
import { getMarkdown } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes: BaseNode[] = []
|
||||
```
|
||||
|
||||
See `packages/markdown-parser/src` for type definitions used by the parser.
|
||||
|
||||
Types for custom component registration are exported too:
|
||||
|
||||
```ts twoslash
|
||||
import type { CustomComponents } from 'markstream-vue'
|
||||
|
||||
const components: CustomComponents = {
|
||||
thinking: {} as any,
|
||||
code_block: {} as any,
|
||||
}
|
||||
```
|
||||
|
||||
## Strongly typed custom components
|
||||
|
||||
You can strongly type your custom components to accept node-specific props. Example:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { CodeBlockNode } from 'markstream-vue'
|
||||
|
||||
const props = defineProps<{ node: CodeBlockNode }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pre class="custom-code">
|
||||
<code :data-lang="props.node.language">{{ props.node.code }}</code>
|
||||
</pre>
|
||||
</template>
|
||||
```
|
||||
|
||||
Then register the component for a specific custom id or globally:
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { setCustomComponents } from 'markstream-vue'
|
||||
|
||||
declare const CustomCodeBlock: Component
|
||||
|
||||
setCustomComponents('docs', {
|
||||
code_block: CustomCodeBlock,
|
||||
})
|
||||
```
|
||||
|
||||
This approach provides full TypeScript checking and code completion for node props inside your components.
|
||||
|
||||
Try this — typed node value example you can use in your code:
|
||||
|
||||
```ts twoslash
|
||||
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||
|
||||
const node: CodeBlockNodeProps['node'] = { type: 'code_block', language: 'ts', code: 'console.log(1)', raw: 'console.log(1)' }
|
||||
```
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
description: Choose between content and nodes, understand the render pipeline, and wire markstream-vue into Vite, VitePress, Nuxt, and streaming UIs.
|
||||
---
|
||||
|
||||
# Usage & API
|
||||
|
||||
This page shows how to wire `markstream-vue` into common stacks, how the parser fits into the renderer, and which docs to visit when something looks odd (reset order, Tailwind/UnoCSS layers, VitePress integration).
|
||||
|
||||
## Choose `content` or `nodes` first
|
||||
|
||||
| Situation | Recommended input |
|
||||
|-----------|-------------------|
|
||||
| Docs pages, static articles, low-frequency updates | `content` |
|
||||
| SSE, token streaming, AI chat, or frequent incremental updates | `nodes` + `final` |
|
||||
| SSR or worker-preparsed content | `nodes` |
|
||||
|
||||
If you only need built-in configuration, stay in this page and [Props & Options](/guide/props). If you need to replace rendering behavior, jump to [Override Built-in Components](/guide/component-overrides).
|
||||
|
||||
If your real goal is an AI chat surface, token stream, or SSE response viewer, use the dedicated [AI Chat & Streaming](/guide/ai-chat-streaming) path instead of piecing the docs together manually.
|
||||
|
||||
If your real goal is a docs site or VitePress theme, use [Docs Site & VitePress](/guide/vitepress-docs-integration) for the end-to-end `content` + `enhanceApp` + CSS setup.
|
||||
|
||||
## Minimal render
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const doc = '# Usage example\n\nSupports **streaming** nodes.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender custom-id="docs" :content="doc" />
|
||||
</template>
|
||||
```
|
||||
|
||||
```css
|
||||
@import 'modern-css-reset';
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
```
|
||||
|
||||
## VitePress + custom tags
|
||||
|
||||
If this is your main use case rather than a one-off example, continue with the dedicated [Docs Site & VitePress](/guide/vitepress-docs-integration) path after this section.
|
||||
|
||||
In VitePress, register your custom node component once in `enhanceApp`, then use `custom-html-tags` on `MarkdownRender` to let the parser emit custom nodes automatically.
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue'
|
||||
// docs/.vitepress/theme/index.ts
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import 'markstream-vue/index.css'
|
||||
|
||||
declare const ThinkingNode: Component
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp() {
|
||||
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```md
|
||||
<!-- in a VitePress page -->
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:custom-html-tags="['thinking']"
|
||||
:content="source"
|
||||
/>
|
||||
```
|
||||
|
||||
Tip: unknown HTML-like tags (such as `<question>`) render as raw HTML elements once closed. Incomplete or malformed fragments stay as plain text so they do not swallow nearby Markdown. Add the tag name to `custom-html-tags` when you want custom node output (type + attrs/content).
|
||||
|
||||
## Parser pipeline
|
||||
|
||||
```ts twoslash
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown()
|
||||
|
||||
const nodes = parseMarkdownToStructure('# Title', md)
|
||||
// pass nodes to <MarkdownRender :nodes="nodes" />
|
||||
```
|
||||
|
||||
- `getMarkdown(msgId?, options?)` returns a configured `markdown-it-ts` instance.
|
||||
- `parseMarkdownToStructure(content, md)` transforms a Markdown string into the AST consumed by the renderer.
|
||||
- Combine with `setCustomComponents(id?, mapping)` to swap node renderers for a given `custom-id`.
|
||||
|
||||
## Streaming recommendation
|
||||
|
||||
For low-frequency updates, passing `content` directly is convenient. For chat-style token streams or long documents, prefer parsing outside the component and passing `nodes` so Vue only reconciles the AST you hand it:
|
||||
|
||||
```ts twoslash
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||
|
||||
const md = getMarkdown('chat')
|
||||
declare const source: string
|
||||
declare const isFinalChunk: boolean
|
||||
const nodes = parseMarkdownToStructure(source, md, { final: isFinalChunk })
|
||||
```
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { BaseNode } from 'markstream-vue'
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const nodes: BaseNode[] = []
|
||||
const isFinalChunk = false
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :nodes="nodes" :final="isFinalChunk" />
|
||||
</template>
|
||||
```
|
||||
|
||||
That avoids reparsing the full Markdown string inside `MarkdownRender` on every tiny content update, which is usually the biggest win for SSE / AI output.
|
||||
|
||||
For a full end-to-end rollout order, peer selection, and chat-specific tuning, continue with [AI Chat & Streaming](/guide/ai-chat-streaming).
|
||||
|
||||
## Component matrix
|
||||
|
||||
For a full list of components and props, visit [Components & node renderers](/guide/components). Highlights:
|
||||
|
||||
- `CodeBlockNode` — Monaco-powered blocks (requires `stream-monaco`).
|
||||
- `MarkdownCodeBlockNode` — Shiki-based lightweight highlighting.
|
||||
- `MermaidBlockNode` — requires `mermaid` ≥ 11 (no extra CSS).
|
||||
- `D2BlockNode` — requires `@terrastruct/d2` (no extra CSS).
|
||||
- `ImageNode` — emits `click`, `load`, `error` for custom previews.
|
||||
|
||||
## Styling reminders
|
||||
|
||||
1. **Reset first** (`modern-css-reset`, `@tailwind base`, `@unocss/reset`), then import `markstream-vue` styles.
|
||||
2. **Use CSS layers** when Tailwind/UnoCSS is active (`@layer components { @import 'markstream-vue/index.css' }`).
|
||||
3. **UNO/Tailwind conflicts** — follow the [Tailwind guide](/guide/tailwind) (includes UnoCSS examples) to prevent utilities from overriding renderer styles.
|
||||
4. **Peer CSS** — KaTeX needs its own CSS; Mermaid/D2 do not. Monaco does not require extra CSS.
|
||||
|
||||
## CSS scoping (important)
|
||||
|
||||
The package CSS is scoped under an internal `.markstream-vue` container to minimize global style conflicts (Tailwind utilities and theme variables included).
|
||||
|
||||
- When you use `MarkdownRender`, you get this container automatically.
|
||||
- If you render exported node components on their own (e.g. `PreCodeNode`, `FootnoteNode`), wrap them with a container element:
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import type { PreCodeNodeProps } from 'markstream-vue'
|
||||
import { PreCodeNode } from 'markstream-vue'
|
||||
|
||||
const node = {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'console.log(1)',
|
||||
raw: 'console.log(1)',
|
||||
} satisfies PreCodeNodeProps['node']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<PreCodeNode :node="node" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
If visuals still look wrong, reproduce the issue inside the playground (`pnpm play`) and cross-check with the troubleshooting guide before filing a bug.
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
description: Integrate markstream-vue into VitePress and docs sites with content mode, enhanceApp registration, trusted custom tags, and safe CSS ordering.
|
||||
---
|
||||
|
||||
# Docs Site & VitePress
|
||||
|
||||
Use this path when you are building a docs site, knowledge base, or content-heavy app where Markdown is mostly rendered as stable page content rather than a live token stream.
|
||||
|
||||
This is the shortest route for VitePress users who need to answer all of these together:
|
||||
|
||||
- should I use `content` or `nodes`
|
||||
- where should `MarkdownRender` and custom nodes be registered
|
||||
- how should `enhanceApp` be used
|
||||
- where should `markstream-vue/index.css` be imported
|
||||
- how do trusted tags such as `thinking` work in docs pages
|
||||
|
||||
## 1. Prefer `content` unless you already own the AST
|
||||
|
||||
For docs sites, `content` is usually the right default:
|
||||
|
||||
| Situation | Recommended input |
|
||||
| --- | --- |
|
||||
| Markdown comes from page content, CMS fields, or normal docs pages | `content` |
|
||||
| You pre-parse on the server or in a content pipeline | `nodes` |
|
||||
| You are building a live stream or AI chat inside the docs site | Use [AI Chat & Streaming](/guide/ai-chat-streaming) instead |
|
||||
|
||||
## 2. Install the smallest docs-friendly set
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue stream-markdown
|
||||
```
|
||||
|
||||
That gives you a good default for docs sites: regular Markdown rendering plus lightweight highlighted code blocks.
|
||||
|
||||
Add more peers only if your docs actually need them:
|
||||
|
||||
- `mermaid` for Mermaid fences
|
||||
- `katex` for math
|
||||
- `stream-monaco` for Monaco-powered code blocks
|
||||
|
||||
## 3. Register renderer and custom nodes in `enhanceApp`
|
||||
|
||||
For VitePress, the safest place to wire global docs-only behavior is `docs/.vitepress/theme/index.ts`.
|
||||
|
||||
```ts
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue'
|
||||
// docs/.vitepress/theme/index.ts
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ThinkingNode from './components/ThinkingNode.vue'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('MarkdownRender', MarkdownRender)
|
||||
|
||||
setCustomComponents('docs', {
|
||||
thinking: ThinkingNode,
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Why this setup helps:
|
||||
|
||||
- `MarkdownRender` becomes available in Markdown pages without local imports
|
||||
- your docs-specific overrides stay scoped to `custom-id="docs"`
|
||||
- theme wiring is kept in one place instead of being repeated in every page
|
||||
|
||||
## 4. Render Markdown in a docs page
|
||||
|
||||
```md
|
||||
<script setup lang="ts">
|
||||
const source = `
|
||||
# Hello docs
|
||||
|
||||
<thinking>
|
||||
This block is rendered by a custom Vue component.
|
||||
</thinking>
|
||||
`
|
||||
</script>
|
||||
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:content="source"
|
||||
:custom-html-tags="['thinking']"
|
||||
/>
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `custom-id="docs"` is what keeps overrides local to the docs site
|
||||
- `custom-html-tags` tells the parser to emit a real custom node instead of leaving the tag as raw HTML
|
||||
- if the source is ordinary docs content, `content` is simpler than manually managing `nodes`
|
||||
|
||||
## 5. Put CSS in a predictable place
|
||||
|
||||
If your docs theme uses custom CSS, import `markstream-vue/index.css` in the theme stylesheet, after your reset or base layer.
|
||||
|
||||
```css
|
||||
/* docs/.vitepress/theme/style.css */
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
}
|
||||
|
||||
[data-custom-id='docs'] .prose {
|
||||
max-width: 72ch;
|
||||
}
|
||||
```
|
||||
|
||||
Practical rules:
|
||||
|
||||
- if your site has a reset, load it before this import
|
||||
- if you use Tailwind or UnoCSS, keep the import inside `@layer components`
|
||||
- if you use KaTeX, also import `katex/dist/katex.min.css`
|
||||
|
||||
Go deeper: [Tailwind Integration & Style Ordering](/guide/tailwind)
|
||||
|
||||
## 6. Trusted tags such as `thinking`
|
||||
|
||||
For docs pages, the recommended flow is still:
|
||||
|
||||
1. allowlist the tag with `custom-html-tags`
|
||||
2. register the node renderer with `setCustomComponents('docs', mapping)`
|
||||
3. keep the page renderer on `custom-id="docs"`
|
||||
|
||||
That is usually enough. Reach for parser hooks only when the source format needs AST reshaping, not just a different renderer.
|
||||
|
||||
Go deeper: [Custom Tags & Advanced Components](/guide/custom-components), [API Reference](/guide/api)
|
||||
|
||||
## 7. Common docs-site mistakes
|
||||
|
||||
- The custom tag renders as raw HTML: you forgot `custom-html-tags`
|
||||
- The component never appears: it was not registered in `enhanceApp`, or the mapping key does not match the tag name
|
||||
- Styles look broken: CSS import order is wrong, or utility layers override the renderer
|
||||
- Overrides leak into other pages: use a scoped `custom-id` instead of a global mapping
|
||||
- Mermaid or Monaco fails in SSR-like environments: check the relevant feature page and client-only boundaries
|
||||
|
||||
Start here when layout or CSS still looks off: [Troubleshooting](/guide/troubleshooting#css-looks-wrong-start-here)
|
||||
|
||||
## Next pages
|
||||
|
||||
- [Installation](/guide/installation) for peer choices
|
||||
- [Usage & Streaming](/guide/usage) for `content` vs `nodes`
|
||||
- [Custom Tags & Advanced Components](/guide/custom-components) for nested custom-tag patterns
|
||||
- [Tailwind Integration & Style Ordering](/guide/tailwind) for utility CSS stacks
|
||||
- [Troubleshooting](/guide/troubleshooting) when styles or peers behave unexpectedly
|
||||
@@ -0,0 +1,642 @@
|
||||
# Vue 2 Components & API
|
||||
|
||||
markstream-vue2 provides the same powerful components as markstream-vue, but built for Vue 2. All components are compatible with Vue 2.6+ (with `@vue/composition-api`) and Vue 2.7+.
|
||||
|
||||
## TypeScript exports
|
||||
|
||||
`markstream-vue2` exports renderer and component prop interfaces:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
CodeBlockNodeProps,
|
||||
D2BlockNodeProps,
|
||||
InfographicBlockNodeProps,
|
||||
MermaidBlockNodeProps,
|
||||
NodeRendererProps,
|
||||
PreCodeNodeProps,
|
||||
} from 'markstream-vue2'
|
||||
import type { CodeBlockNode } from 'stream-markdown-parser'
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `NodeRendererProps` matches `<MarkdownRender>` props.
|
||||
- `CodeBlockNodeProps`, `MermaidBlockNodeProps`, `D2BlockNodeProps`, `InfographicBlockNodeProps`, and `PreCodeNodeProps` all use `CodeBlockNode` for `node` (use `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` to route specialized renderers).
|
||||
|
||||
## Main Component: MarkdownRender
|
||||
|
||||
The primary component for rendering markdown content in Vue 2.
|
||||
|
||||
### Props
|
||||
|
||||
`MarkdownRender` in Vue 2 mirrors the Vue 3 renderer props. In templates, use kebab-case.
|
||||
|
||||
#### Core props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `string` | - | Markdown content to render |
|
||||
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]` from the parser) |
|
||||
| `custom-id` | `string` | - | Identifier for scoping custom components and CSS (`[data-custom-id="..."]`) |
|
||||
| `final` | `boolean` | `false` | Marks the input as end-of-stream; stops emitting streaming `loading` nodes |
|
||||
| `parse-options` | `ParseOptions` | - | Parser options and token hooks (only when `content` is provided) |
|
||||
| `custom-html-tags` | `string[]` | - | HTML-like tags emitted as custom nodes (e.g. `thinking`) |
|
||||
| `custom-markdown-it` | `(md: MarkdownIt) => MarkdownIt` | - | Customize the internal MarkdownIt instance |
|
||||
| `debug-performance` | `boolean` | `false` | Log parse/render timing and virtualization stats (dev only) |
|
||||
| `is-dark` | `boolean` | `false` | Theme flag forwarded to heavy nodes; adds `.dark` to the root container |
|
||||
| `index-key` | `number \| string` | - | Key prefix when rendering multiple instances in lists |
|
||||
| `typewriter` | `boolean` | `true` | Enable the non-code-node enter transition |
|
||||
| `show-tooltips` | `boolean` | `true` | Global tooltip switch for `LinkNode` and code block nodes |
|
||||
|
||||
#### Streaming & heavy-node toggles
|
||||
|
||||
| Prop | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `render-code-blocks-as-pre` | `false` | Render non-Mermaid/Infographic/D2 code blocks as `<pre><code>` |
|
||||
| `code-block-stream` | `true` | Stream code block updates as content arrives |
|
||||
| `viewport-priority` | `true` | Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport |
|
||||
| `defer-nodes-until-visible` | `true` | Render heavy nodes as placeholders until visible (non-virtualized mode only) |
|
||||
|
||||
#### Performance (virtualization & batching)
|
||||
|
||||
| Prop | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `max-live-nodes` | `320` | Max fully rendered nodes kept in DOM (set `0` to disable virtualization) |
|
||||
| `live-node-buffer` | `60` | Overscan buffer around the focus range |
|
||||
| `batch-rendering` | `true` | Incremental batch rendering when virtualization is disabled |
|
||||
| `initial-render-batch-size` | `40` | Nodes rendered immediately before batching starts |
|
||||
| `render-batch-size` | `80` | Nodes rendered per batch tick |
|
||||
| `render-batch-delay` | `16` | Extra delay (ms) before each batch after rAF |
|
||||
| `render-batch-budget-ms` | `6` | Time budget (ms) before adaptive batch sizes shrink |
|
||||
| `render-batch-idle-timeout-ms` | `120` | Timeout (ms) for `requestIdleCallback` slices |
|
||||
|
||||
#### Global code block options
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `code-block-dark-theme` | `any` | Monaco dark theme object forwarded to every `CodeBlockNode` |
|
||||
| `code-block-light-theme` | `any` | Monaco light theme object forwarded to every `CodeBlockNode` |
|
||||
| `code-block-monaco-options` | `Record<string, any>` | Options forwarded to `stream-monaco`, including diff hover-action settings like `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` |
|
||||
| `code-block-min-width` | `string \| number` | Min width forwarded to `CodeBlockNode` |
|
||||
| `code-block-max-width` | `string \| number` | Max width forwarded to `CodeBlockNode` |
|
||||
| `code-block-props` | `Record<string, any>` | Extra props forwarded to every `CodeBlockNode` |
|
||||
| `mermaid-props` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to Mermaid fences and custom `mermaid` renderers |
|
||||
| `d2-props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to D2 fences and custom `d2` renderers |
|
||||
| `infographic-props` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to infographic fences and custom `infographic` renderers |
|
||||
| `themes` | `string[]` | Theme list forwarded to `stream-monaco` |
|
||||
|
||||
#### Heavy renderer prop forwarding
|
||||
|
||||
`MarkdownRender` can tune heavy blocks directly:
|
||||
|
||||
```vue
|
||||
<MarkdownRender
|
||||
:content="markdown"
|
||||
:viewport-priority="true"
|
||||
:mermaid-props="{
|
||||
showHeader: false,
|
||||
renderDebounceMs: 180,
|
||||
previewPollDelayMs: 500
|
||||
}"
|
||||
:d2-props="{ progressiveIntervalMs: 500 }"
|
||||
:infographic-props="{ showHeader: false }"
|
||||
/>
|
||||
```
|
||||
|
||||
Streaming notes:
|
||||
- Keep `viewport-priority` enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
|
||||
- For high-frequency SSE, prefer passing `nodes` instead of reparsing the full `content` string every chunk.
|
||||
- Common Mermaid tuning keys: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`.
|
||||
|
||||
#### Events
|
||||
|
||||
- `@copy`, `@handleArtifactClick`, `@click`, `@mouseover`, `@mouseout`
|
||||
|
||||
### Usage
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: '# Hello Vue 2!'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender
|
||||
custom-id="docs"
|
||||
:content="markdown"
|
||||
:max-live-nodes="150"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Code Block Components
|
||||
|
||||
### MarkdownCodeBlockNode
|
||||
|
||||
Lightweight code highlighting using Shiki.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { MarkdownCodeBlockNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownCodeBlockNode },
|
||||
data() {
|
||||
return {
|
||||
codeNode: {
|
||||
type: 'code_block',
|
||||
language: 'javascript',
|
||||
code: 'const hello = "world"',
|
||||
raw: 'const hello = "world"'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCopy() {
|
||||
alert('Code copied!')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<MarkdownCodeBlockNode
|
||||
:node="codeNode"
|
||||
:show-copy-button="true"
|
||||
@copy="handleCopy"
|
||||
>
|
||||
<template #header-right>
|
||||
<span class="lang-badge">{{ codeNode.language }}</span>
|
||||
</template>
|
||||
</MarkdownCodeBlockNode>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### CodeBlockNode
|
||||
|
||||
Feature-rich Monaco-powered code blocks.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { CodeBlockNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { CodeBlockNode },
|
||||
data() {
|
||||
return {
|
||||
codeNode: {
|
||||
type: 'code_block',
|
||||
language: 'typescript',
|
||||
code: 'const greeting: string = "Hello"',
|
||||
raw: 'const greeting: string = "Hello"'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCopy(code) {
|
||||
console.log('Code copied:', code)
|
||||
},
|
||||
handlePreviewCode(artifact) {
|
||||
console.log('Preview code:', artifact)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<CodeBlockNode
|
||||
:node="codeNode"
|
||||
:monaco-options="{ fontSize: 14, theme: 'vs-dark' }"
|
||||
:stream="true"
|
||||
@copy="handleCopy"
|
||||
@preview-code="handlePreviewCode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Math Components
|
||||
|
||||
### MathBlockNode
|
||||
|
||||
Renders block-level math formulas with KaTeX.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { MathBlockNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MathBlockNode },
|
||||
data() {
|
||||
return {
|
||||
mathNode: {
|
||||
type: 'math_block',
|
||||
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
||||
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<MathBlockNode
|
||||
:node="mathNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### MathInlineNode
|
||||
|
||||
Renders inline math formulas.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { MathInlineNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MathInlineNode },
|
||||
data() {
|
||||
return {
|
||||
inlineMathNode: {
|
||||
type: 'math_inline',
|
||||
content: 'E = mc^2',
|
||||
raw: 'E = mc^2'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<p>
|
||||
The formula is:
|
||||
<MathInlineNode :node="inlineMathNode" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
### MermaidBlockNode
|
||||
|
||||
Progressive Mermaid diagram rendering.
|
||||
|
||||
#### Quick reference
|
||||
- **Tooltip control**: `showTooltips` defaults to `true`; set `false` to disable header action tooltips.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { MermaidBlockNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MermaidBlockNode },
|
||||
data() {
|
||||
return {
|
||||
mermaidNode: {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: `graph TD
|
||||
A[Start] --> B{Is it working?}
|
||||
B -->|Yes| C[Great!]`,
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onExport(ev) {
|
||||
console.log('Mermaid SVG:', ev.svgString)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<MermaidBlockNode
|
||||
:node="mermaidNode"
|
||||
:is-strict="true"
|
||||
:render-debounce-ms="180"
|
||||
:preview-poll-delay-ms="500"
|
||||
:show-tooltips="true"
|
||||
@export="onExport"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Useful streaming props:
|
||||
- `render-debounce-ms`
|
||||
- `content-stable-delay-ms`
|
||||
- `preview-poll-delay-ms`
|
||||
- `preview-poll-max-delay-ms`
|
||||
- `preview-poll-max-attempts`
|
||||
|
||||
## D2 Diagrams
|
||||
|
||||
### D2BlockNode
|
||||
|
||||
Progressive D2 diagram rendering with source fallback.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { D2BlockNode } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { D2BlockNode },
|
||||
data() {
|
||||
return {
|
||||
d2Node: {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: `direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query`,
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markstream-vue">
|
||||
<D2BlockNode
|
||||
:node="d2Node"
|
||||
:progressive-interval-ms="600"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### setCustomComponents
|
||||
|
||||
Register custom node renderers for specific markdown nodes.
|
||||
|
||||
```js
|
||||
import { setCustomComponents } from 'markstream-vue2'
|
||||
|
||||
// Define custom component
|
||||
const CustomHeading = {
|
||||
name: 'CustomHeading',
|
||||
props: ['node', 'indexKey', 'customId'],
|
||||
render(h) {
|
||||
const level = this.node.level || 1
|
||||
return h(`h${level}`, {
|
||||
class: 'custom-heading',
|
||||
attrs: { 'data-custom-id': this.customId }
|
||||
}, this.node.children.map(c => c.content))
|
||||
}
|
||||
}
|
||||
|
||||
// Register globally
|
||||
setCustomComponents('docs', {
|
||||
heading: CustomHeading
|
||||
})
|
||||
```
|
||||
|
||||
### getMarkdown
|
||||
|
||||
Get a configured markdown-it instance.
|
||||
|
||||
```js
|
||||
import { getMarkdown } from 'markstream-vue2'
|
||||
|
||||
const md = getMarkdown('my-msg-id', {
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
})
|
||||
|
||||
const tokens = md.parse('# Hello World')
|
||||
```
|
||||
|
||||
### parseMarkdownToStructure
|
||||
|
||||
Parse markdown string to AST structure.
|
||||
|
||||
```js
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue2'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)
|
||||
|
||||
// Use with MarkdownRender
|
||||
// <MarkdownRender :nodes="nodes" />
|
||||
```
|
||||
|
||||
### enableKatex / enableMermaid / enableD2
|
||||
|
||||
(Re)enable feature loaders for KaTeX, Mermaid, and D2. Default loaders are already on; call these only if you disabled them earlier or want to override the loader (for example, using a CDN build).
|
||||
|
||||
```js
|
||||
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue2'
|
||||
|
||||
// Enable KaTeX loader
|
||||
enableKatex()
|
||||
|
||||
// Enable Mermaid loader
|
||||
enableMermaid()
|
||||
|
||||
// Enable D2 loader
|
||||
enableD2()
|
||||
```
|
||||
|
||||
## Custom Component API
|
||||
|
||||
### Props Interface
|
||||
|
||||
All custom node components receive these props:
|
||||
|
||||
```ts
|
||||
interface NodeComponentProps {
|
||||
node: ParsedNode // The parsed node data
|
||||
indexKey: number | string // Unique key for the node
|
||||
customId?: string // Custom ID for scoping
|
||||
isDark?: boolean // Forwarded theme flag (from MarkdownRender)
|
||||
typewriter?: boolean // Forwarded typewriter flag (non-code nodes only)
|
||||
loading?: boolean // Streaming/loading state (from node.loading)
|
||||
}
|
||||
```
|
||||
|
||||
### Example Custom Component
|
||||
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomParagraph',
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
indexKey: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
customId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tag() {
|
||||
return 'p'
|
||||
},
|
||||
classes() {
|
||||
return [
|
||||
'custom-paragraph',
|
||||
`custom-paragraph-${this.indexKey}`
|
||||
]
|
||||
},
|
||||
attrs() {
|
||||
return {
|
||||
'data-custom-id': this.customId,
|
||||
'data-node-type': this.node.type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="classes"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<slot>
|
||||
<template v-for="(child, i) in node.children">
|
||||
<span v-if="child.type === 'text'" :key="i">
|
||||
{{ child.content }}
|
||||
</span>
|
||||
<!-- Handle other node types... -->
|
||||
</template>
|
||||
</slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-paragraph {
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Streaming Support
|
||||
|
||||
markstream-vue2 supports streaming markdown content with the `loading` state on nodes:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
streamingContent: '',
|
||||
fullContent: `# Streaming Demo
|
||||
|
||||
This content streams in **small chunks**.
|
||||
|
||||
\`\`\`javascript
|
||||
console.log('Streaming...')
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.startStreaming()
|
||||
},
|
||||
methods: {
|
||||
startStreaming() {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i < this.fullContent.length) {
|
||||
this.streamingContent += this.fullContent[i]
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MarkdownRender :content="streamingContent" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
markstream-vue2 includes full TypeScript definitions. For Vue 2.6.x, configure your `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["@vue/composition-api", "markstream-vue2"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Vue 2.7+, types are included automatically:
|
||||
|
||||
```ts
|
||||
import type { ParsedNode } from 'markstream-vue2'
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
// Your component with proper typing
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { MarkdownRender },
|
||||
setup() {
|
||||
const markdown = ref('# Hello')
|
||||
const nodes = ref<ParsedNode[]>([])
|
||||
|
||||
return { markdown, nodes }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Differences from Vue 3 Version
|
||||
|
||||
The Vue 2 version maintains API compatibility with the Vue 3 version with these considerations:
|
||||
|
||||
1. **Composition API**: Requires Vue 2.7+ or `@vue/composition-api` for Vue 2.6.x
|
||||
2. **Slots**: Use Vue 2 scoped slot syntax
|
||||
3. **Event names**: Use kebab-case for event names in templates
|
||||
4. **v-model**: No changes needed, works the same way
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [Vue 2 Quick Start](/guide/vue2-quick-start) for setup examples
|
||||
- Explore [Vue 3 Components](/guide/components) for more component examples (API is the same)
|
||||
- Check [Usage & API](/guide/usage) for advanced patterns
|
||||
@@ -0,0 +1,479 @@
|
||||
# Vue 2 Installation
|
||||
|
||||
Install markstream-vue2 with pnpm, npm or yarn.
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue2
|
||||
# or
|
||||
npm install markstream-vue2
|
||||
# or
|
||||
yarn add markstream-vue2
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
markstream-vue2 requires:
|
||||
- **Vue 2.6.14+** (Vue 2.7 is recommended for better TypeScript support)
|
||||
- **@vue/composition-api** (if using Vue 2.6.x)
|
||||
|
||||
## Composition API compatibility (Vue 2.6 / 2.7 / 3.x)
|
||||
|
||||
| Vue version | Composition API availability | What to install | How to import |
|
||||
|-------------|------------------------------|-----------------|---------------|
|
||||
| **2.6.x** | Not built-in | `@vue/composition-api` | `import { ref, computed, defineComponent } from '@vue/composition-api'` |
|
||||
| **2.7.x** | Built-in | None | `import { ref, computed, defineComponent } from 'vue'` |
|
||||
| **3.x** | Built-in | None | `import { ref, computed, defineComponent } from 'vue'` |
|
||||
|
||||
Notes:
|
||||
- For **Vue 2.6.x**, you must install and **register the plugin** (`Vue.use(@vue/composition-api)`), otherwise you will get runtime errors.
|
||||
- For **Vue 2.7.x**, **do not** install `@vue/composition-api` — it is already included in Vue 2.7.
|
||||
- For **Vue 3.x**, use **markstream-vue** (not markstream-vue2).
|
||||
|
||||
## Quick start by version (dependencies + entry)
|
||||
|
||||
### Vue 2.6.x
|
||||
|
||||
Dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue2 vue@2.6.14 vue-template-compiler@2.6.14 @vue/composition-api
|
||||
```
|
||||
|
||||
App entry:
|
||||
|
||||
```ts
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
import MarkdownRender, { VueRendererMarkdown } from 'markstream-vue2'
|
||||
import Vue from 'vue'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
Vue.use(VueCompositionAPI)
|
||||
Vue.use(VueRendererMarkdown)
|
||||
|
||||
new Vue({
|
||||
render: h => h(MarkdownRender, { props: { content: '# Vue 2.6' } }),
|
||||
}).$mount('#app')
|
||||
```
|
||||
|
||||
Composition API usage in components:
|
||||
|
||||
```ts
|
||||
import { defineComponent, ref } from '@vue/composition-api'
|
||||
```
|
||||
|
||||
Example in this repo:
|
||||
- `playground-vue2-cli` (Vue 2.6 + Vue CLI / Webpack 4)
|
||||
|
||||
Path and start command:
|
||||
|
||||
```bash
|
||||
pnpm -C playground-vue2-cli dev
|
||||
```
|
||||
|
||||
From repo root you can also run:
|
||||
|
||||
```bash
|
||||
pnpm play:vue2-cli
|
||||
```
|
||||
|
||||
### Vue 2.7.x
|
||||
|
||||
Dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue2 vue@2.7.16 vue-template-compiler@2.7.16
|
||||
```
|
||||
|
||||
App entry:
|
||||
|
||||
```ts
|
||||
import MarkdownRender, { VueRendererMarkdown } from 'markstream-vue2'
|
||||
import Vue from 'vue'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
Vue.use(VueRendererMarkdown)
|
||||
|
||||
new Vue({
|
||||
render: h => h(MarkdownRender, { props: { content: '# Vue 2.7' } }),
|
||||
}).$mount('#app')
|
||||
```
|
||||
|
||||
Composition API usage in components:
|
||||
|
||||
```ts
|
||||
import { defineComponent, ref } from 'vue'
|
||||
```
|
||||
|
||||
Example in this repo:
|
||||
- `playground-vue2` (Vue 2.7 + Vite)
|
||||
|
||||
Path and start command:
|
||||
|
||||
```bash
|
||||
pnpm -C playground-vue2 dev
|
||||
```
|
||||
|
||||
From repo root you can also run:
|
||||
|
||||
```bash
|
||||
pnpm play:vue2
|
||||
```
|
||||
|
||||
### Vue 3.x (use markstream-vue)
|
||||
|
||||
Dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue vue@^3
|
||||
```
|
||||
|
||||
App entry:
|
||||
|
||||
```ts
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { createApp, h } from 'vue'
|
||||
import 'markstream-vue/index.css'
|
||||
|
||||
createApp({
|
||||
render: () => h(MarkdownRender, { content: '# Vue 3' }),
|
||||
}).mount('#app')
|
||||
```
|
||||
|
||||
If your workspace also installs Vue 3, make sure `vue-demi` targets Vue 2:
|
||||
|
||||
```bash
|
||||
pnpm vue-demi-switch 2
|
||||
```
|
||||
|
||||
If you cannot run `vue-demi-switch` (or want a per-app fix), alias `vue-demi` to the Vue 2 build in your bundler config. This avoids errors like `defineComponent is not a function`:
|
||||
|
||||
```js
|
||||
// vue.config.js / webpack config
|
||||
module.exports = {
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue-demi$': 'vue-demi/lib/v2/index.cjs',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Peer Dependencies
|
||||
|
||||
markstream-vue2 supports various features through optional peer dependencies. Install only what you need:
|
||||
|
||||
| Feature | Required Packages | Install Command |
|
||||
|---------|------------------|-----------------|
|
||||
| Shiki code blocks (`MarkdownCodeBlockNode`) | `stream-markdown` | `pnpm add stream-markdown` |
|
||||
| Monaco Editor (full code block features) | `stream-monaco` | `pnpm add stream-monaco` |
|
||||
| Mermaid Diagrams | `mermaid` | `pnpm add mermaid` |
|
||||
| D2 Diagrams | `@terrastruct/d2` | `pnpm add @terrastruct/d2` |
|
||||
| Math Rendering (KaTeX) | `katex` | `pnpm add katex` |
|
||||
|
||||
## Vue 2.6.x Setup
|
||||
|
||||
If you're using Vue 2.6.x, you need to install and configure `@vue/composition-api`:
|
||||
|
||||
```bash
|
||||
pnpm add @vue/composition-api
|
||||
```
|
||||
|
||||
Then in your app entry:
|
||||
|
||||
```ts
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
import Vue from 'vue'
|
||||
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
Using Composition API in Vue 2.6 components:
|
||||
|
||||
```ts
|
||||
import { defineComponent, ref } from '@vue/composition-api'
|
||||
```
|
||||
|
||||
## Vue 2.7.x Setup (no plugin required)
|
||||
|
||||
Vue 2.7 has Composition API built-in. Do **not** install `@vue/composition-api`.
|
||||
|
||||
```ts
|
||||
import { defineComponent, ref } from 'vue'
|
||||
```
|
||||
|
||||
## Feature loaders (Mermaid / KaTeX / D2)
|
||||
|
||||
After installing optional peers, the default loaders are already enabled. Only call these helpers if you disabled them earlier or need a custom loader (for example, a CDN build):
|
||||
|
||||
```ts
|
||||
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue2'
|
||||
|
||||
// optional: re-enable or override loaders
|
||||
enableMermaid()
|
||||
enableKatex()
|
||||
enableD2()
|
||||
```
|
||||
|
||||
Also remember required CSS (when the feature is used):
|
||||
|
||||
```ts
|
||||
import 'markstream-vue2/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
Monaco (`stream-monaco`) does not require a separate CSS import.
|
||||
|
||||
Note: `markstream-vue2/index.css` is scoped under an internal `.markstream-vue2` container to reduce global style conflicts. `MarkdownRender` renders inside that container by default. If you render node components standalone, wrap them with `<div class="markstream-vue2">...</div>`.
|
||||
|
||||
## Vue CLI (Webpack 4) Notes
|
||||
|
||||
Vue CLI 4 uses **Webpack 4** by default, and Webpack 4 doesn't support `package.json#exports`. That commonly breaks:
|
||||
|
||||
- `import 'markstream-vue2/index.css'`
|
||||
- `import('mermaid')`
|
||||
- Vite-style worker imports: `?worker` / `?worker&inline`
|
||||
|
||||
Recommended setup (see `playground-vue2-cli`):
|
||||
|
||||
1) Import CSS via the real file path (no `exports` needed):
|
||||
|
||||
```ts
|
||||
import 'markstream-vue2/dist/index.css'
|
||||
```
|
||||
|
||||
2) For Mermaid, prefer the CDN global build (avoids Webpack 4 `exports` / ESM chains):
|
||||
|
||||
```html
|
||||
<!-- public/index.html -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
```
|
||||
|
||||
3) For KaTeX / Mermaid workers, prefer CDN workers to avoid Webpack 4 worker limitations:
|
||||
|
||||
```ts
|
||||
import { createKaTeXWorkerFromCDN, createMermaidWorkerFromCDN, setKaTeXWorker, setMermaidWorker } from 'markstream-vue2'
|
||||
|
||||
const { worker: katexWorker } = createKaTeXWorkerFromCDN({
|
||||
mode: 'classic',
|
||||
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.js',
|
||||
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/contrib/mhchem.min.js',
|
||||
})
|
||||
if (katexWorker)
|
||||
setKaTeXWorker(katexWorker)
|
||||
|
||||
const { worker: mermaidWorker } = createMermaidWorkerFromCDN({
|
||||
mode: 'classic',
|
||||
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
|
||||
})
|
||||
if (mermaidWorker)
|
||||
setMermaidWorker(mermaidWorker)
|
||||
```
|
||||
|
||||
If you want to keep using subpath imports like `markstream-vue2/index.css`, add `resolve.alias` mappings in `vue.config.js` (see `playground-vue2-cli/vue.config.js`).
|
||||
|
||||
### Code blocks: prefer stream-markdown override
|
||||
|
||||
In Webpack 4, Monaco integration (`stream-monaco`) is prone to edge cases (worker resolution, ESM entrypoints, service registration, and Shiki/TextMate regex engine init issues). If your goal is “render code blocks with highlighting”, the most reliable approach is:
|
||||
|
||||
1) Install Shiki renderer deps:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
```
|
||||
|
||||
2) Override `code_block` to use `MarkdownCodeBlockNode` (or your own component):
|
||||
|
||||
```ts
|
||||
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
|
||||
|
||||
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||
```
|
||||
|
||||
3) Webpack config pitfalls:
|
||||
|
||||
- If you use `webpack.IgnorePlugin` for optional dependencies, ensure `stream-markdown` is **not** ignored.
|
||||
- `stream-markdown` is ESM-only. In a CJS `vue.config.js`, `require.resolve('stream-markdown')` can fail even when installed. Use a filesystem fallback (check `node_modules/stream-markdown`) and alias it to `dist/index.js`. See `playground-vue2-cli/vue.config.js`.
|
||||
|
||||
If you're in a monorepo/workspace (common with pnpm), make sure your app uses a **single Vue 2 runtime instance**. Otherwise you may see runtime warnings like:
|
||||
`provide() can only be used inside setup()` / `onMounted is called when there is no active component instance` (caused by duplicated Vue copies and mismatched Composition API context). Fix this by pinning `vue$` to your project's Vue 2 runtime in Webpack (see `playground-vue2-cli/vue.config.js`).
|
||||
|
||||
## Tailwind CSS Support
|
||||
|
||||
If your app uses Tailwind and you want to avoid duplicate utility CSS, import the Tailwind-ready output instead:
|
||||
|
||||
```ts
|
||||
import 'markstream-vue2/index.tailwind.css'
|
||||
```
|
||||
|
||||
And include the extracted class list in `tailwind.config.js`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,ts,vue}',
|
||||
require('markstream-vue2/tailwind'),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This approach ensures that Tailwind includes all the utility classes used by markstream-vue2 in its purge process, resulting in a smaller final bundle size.
|
||||
|
||||
## Troubleshooting (Common Runtime Errors)
|
||||
|
||||
### `defineComponent is not a function`
|
||||
Cause: `vue-demi` is in Vue 3 mode but the app runs Vue 2.x.
|
||||
Fix (pick one):
|
||||
- Run `pnpm vue-demi-switch 2`.
|
||||
- Or alias `vue-demi$` to `vue-demi/lib/v2/index.cjs` in your bundler config (see above).
|
||||
|
||||
### `provide() can only be used inside setup()` / `onMounted is called when there is no active component instance`
|
||||
Cause: duplicate Vue 2 runtime instances in monorepo/pnpm workspace.
|
||||
Fix: ensure a **single Vue 2 instance** is used across the app and linked packages:
|
||||
- Alias `vue$` to your app's Vue 2 runtime (see `playground-vue2-cli/vue.config.js`).
|
||||
- In pnpm workspaces, add `overrides` for `playground-vue2-cli>vue` to pin Vue 2.
|
||||
|
||||
### `Vue packages version mismatch`
|
||||
Cause: `vue` and `vue-template-compiler` versions differ.
|
||||
Fix: align them (e.g. both `2.6.14` or both `2.7.16`). In pnpm, you can use `packageExtensions` or `overrides`.
|
||||
|
||||
### `Cannot read properties of undefined (reading 'props')`
|
||||
Cause: Vue 2.6 + Composition API missing `_setupProxy` patch, or Composition API not installed.
|
||||
Fix:
|
||||
- Ensure `@vue/composition-api` is installed and `Vue.use(VueCompositionAPI)` is executed before using markstream-vue2.
|
||||
- Upgrade to the latest `markstream-vue2` build in this repo (it patches `_setupProxy` for Vue 2.6).
|
||||
|
||||
### Quick Install: All Features
|
||||
|
||||
To enable all features at once:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown stream-monaco mermaid @terrastruct/d2 katex
|
||||
# or
|
||||
npm install stream-markdown stream-monaco mermaid @terrastruct/d2 katex
|
||||
```
|
||||
|
||||
### Feature Details
|
||||
|
||||
#### Code Syntax Highlighting
|
||||
|
||||
Requires `stream-markdown`:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
```
|
||||
|
||||
`stream-markdown` bundles the Shiki runtime used by `MarkdownCodeBlockNode`. To use Shiki inside `MarkdownRender`, override the `code_block` renderer (or render `MarkdownCodeBlockNode` directly).
|
||||
|
||||
```js
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
|
||||
|
||||
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||
```
|
||||
|
||||
#### Monaco Editor
|
||||
|
||||
For full code block functionality (copy button, font size controls, expand/collapse):
|
||||
|
||||
```bash
|
||||
pnpm add stream-monaco
|
||||
```
|
||||
|
||||
Without `stream-monaco`, code blocks will render but interactive buttons may not work.
|
||||
|
||||
#### Mermaid Diagrams
|
||||
|
||||
For rendering Mermaid diagrams:
|
||||
|
||||
```bash
|
||||
pnpm add mermaid
|
||||
```
|
||||
|
||||
#### D2 Diagrams
|
||||
|
||||
For rendering D2 diagrams:
|
||||
|
||||
```bash
|
||||
pnpm add @terrastruct/d2
|
||||
```
|
||||
|
||||
#### KaTeX Math Rendering
|
||||
|
||||
For math formula rendering:
|
||||
|
||||
```bash
|
||||
pnpm add katex
|
||||
```
|
||||
|
||||
Also import the KaTeX CSS in your app entry:
|
||||
|
||||
```ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
## Quick Test
|
||||
|
||||
Import and render a simple markdown string:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
md: '# Hello from markstream-vue2!'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Using with Composition API (Vue 2.7+)
|
||||
|
||||
If you're using Vue 2.7 or have `@vue/composition-api` installed:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { defineComponent, ref } from '@vue/composition-api' // or 'vue' for 2.7
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
setup() {
|
||||
const md = ref('# Hello from markstream-vue2 with Composition API!')
|
||||
return { md }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
markstream-vue2 includes TypeScript definitions. For Vue 2.6.x, you may need to configure your `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["@vue/composition-api"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Vue 2.7+, types are included automatically.
|
||||
@@ -0,0 +1,471 @@
|
||||
# Vue 2 Quick Start
|
||||
|
||||
Get started with markstream-vue2 in your Vue 2 project.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Installation
|
||||
|
||||
First, install the package:
|
||||
|
||||
```bash
|
||||
pnpm add markstream-vue2
|
||||
```
|
||||
|
||||
### 2. Import Styles
|
||||
|
||||
In your main entry file (e.g., `main.js` or `main.ts`):
|
||||
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
// For Vue 2.6.x, also install and configure @vue/composition-api
|
||||
// import VueCompositionAPI from '@vue/composition-api'
|
||||
// Vue.use(VueCompositionAPI)
|
||||
|
||||
new Vue({
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
```
|
||||
|
||||
### 3. Use the Component
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
markdown: `# Hello Vue 2!
|
||||
|
||||
This is **markstream-vue2** - a streaming-friendly Markdown renderer for Vue 2.
|
||||
|
||||
## Features
|
||||
|
||||
- Code syntax highlighting
|
||||
- Mermaid diagrams
|
||||
- Math formulas
|
||||
- And much more!
|
||||
|
||||
\`\`\`javascript
|
||||
console.log('Hello from Vue 2!')
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<MarkdownRender :content="markdown" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Using with Vue 2.7+ (Composition API)
|
||||
|
||||
Vue 2.7 includes built-in Composition API support:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
setup() {
|
||||
const markdown = ref(`# Hello Vue 2.7!
|
||||
|
||||
This uses the Composition API.
|
||||
|
||||
\`\`\`javascript
|
||||
const message = 'Hello from Vue 2.7!'
|
||||
console.log(message)
|
||||
\`\`\`
|
||||
`)
|
||||
return { markdown }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<MarkdownRender :content="markdown" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Using with Vue 2.6.x (@vue/composition-api)
|
||||
|
||||
For Vue 2.6.x, install `@vue/composition-api`:
|
||||
|
||||
```bash
|
||||
pnpm add @vue/composition-api
|
||||
```
|
||||
|
||||
```js
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
// main.js
|
||||
import Vue from 'vue'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
Then use it the same way as Vue 2.7:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { defineComponent, ref } from '@vue/composition-api'
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
setup() {
|
||||
const markdown = ref(`# Hello Vue 2.6!
|
||||
|
||||
This uses @vue/composition-api.
|
||||
`)
|
||||
return { markdown }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<MarkdownRender :content="markdown" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Enabling Optional Features
|
||||
|
||||
### Code Syntax Highlighting
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add stream-markdown
|
||||
```
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
|
||||
|
||||
// Use Shiki-based code blocks inside MarkdownRender
|
||||
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: `\`\`\`javascript
|
||||
const hello = 'world'
|
||||
console.log(hello)
|
||||
\`\`\``
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Mermaid Diagrams
|
||||
|
||||
Install mermaid:
|
||||
|
||||
```bash
|
||||
pnpm add mermaid
|
||||
```
|
||||
|
||||
Import styles. The loader is enabled by default; call `enableMermaid()` only if you disabled it or need a custom loader:
|
||||
|
||||
```js
|
||||
import { enableMermaid } from 'markstream-vue2'
|
||||
// main.js
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
// optional: re-enable or override loader
|
||||
enableMermaid()
|
||||
```
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: `#### Mermaid Diagram
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Is it working?}
|
||||
B -->|Yes| C[Great!]
|
||||
B -->|No| D[Keep trying]
|
||||
\`\`\``
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### D2 Diagrams
|
||||
|
||||
Install D2:
|
||||
|
||||
```bash
|
||||
pnpm add @terrastruct/d2
|
||||
```
|
||||
|
||||
Import styles. The loader is enabled by default; call `enableD2()` only if you disabled it or need a custom loader:
|
||||
|
||||
```js
|
||||
import { enableD2 } from 'markstream-vue2'
|
||||
// main.js
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
// optional: re-enable or override loader
|
||||
enableD2()
|
||||
```
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: `#### D2 Diagram
|
||||
|
||||
\`\`\`d2
|
||||
direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query
|
||||
DB -> API: rows
|
||||
API -> Client: response
|
||||
\`\`\``
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Math Formulas (KaTeX)
|
||||
|
||||
Install katex:
|
||||
|
||||
```bash
|
||||
pnpm add katex
|
||||
```
|
||||
|
||||
Import styles. The loader is enabled by default; call `enableKatex()` only if you disabled it or need a custom loader:
|
||||
|
||||
```js
|
||||
import { enableKatex } from 'markstream-vue2'
|
||||
// main.js
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
// optional: re-enable or override loader
|
||||
enableKatex()
|
||||
```
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: `#### Math Example
|
||||
|
||||
Inline math: $E = mc^2$
|
||||
|
||||
Block math:
|
||||
|
||||
$$
|
||||
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
|
||||
$$`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Custom Components
|
||||
|
||||
You can customize how specific nodes are rendered using `setCustomComponents`:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue2'
|
||||
|
||||
// Create a custom heading component
|
||||
const CustomHeading = {
|
||||
name: 'CustomHeading',
|
||||
props: ['node'],
|
||||
render(h) {
|
||||
const level = this.node.level || 1
|
||||
const Tag = `h${level}`
|
||||
return h(Tag, { class: 'custom-heading' }, this.node.children.map(c => c.content))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: `# Custom Heading
|
||||
|
||||
This heading is rendered with a custom component.
|
||||
`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Register custom component
|
||||
setCustomComponents('my-app', {
|
||||
heading: CustomHeading
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MarkdownRender
|
||||
custom-id="my-app"
|
||||
:content="markdown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-heading {
|
||||
color: #e11d48;
|
||||
border-bottom: 2px solid #e11d48;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Streaming Content
|
||||
|
||||
markstream-vue2 supports streaming markdown content, which is useful for AI-generated content:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import MarkdownRender from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
components: { MarkdownRender },
|
||||
data() {
|
||||
return {
|
||||
markdown: '',
|
||||
fullText: `# Streaming Demo
|
||||
|
||||
This content is being streamed **in small chunks**.
|
||||
|
||||
## Features
|
||||
|
||||
1. Progressive rendering
|
||||
2. No layout shift
|
||||
3. Smooth animations
|
||||
|
||||
\`\`\`javascript
|
||||
const streaming = true
|
||||
console.log('Streaming enabled:', streaming)
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startStreaming() {
|
||||
this.markdown = ''
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i < this.fullText.length) {
|
||||
this.markdown += this.fullText[i]
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button @click="startStreaming">
|
||||
Start Streaming
|
||||
</button>
|
||||
<MarkdownRender :content="markdown" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## VitePress Integration (Vue 2)
|
||||
|
||||
For VitePress with Vue 2, you can use markstream-vue2 in your custom theme:
|
||||
|
||||
```js
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue2'
|
||||
// docs/.vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import 'markstream-vue2/index.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
// Register the component globally
|
||||
app.component('MarkdownRender', MarkdownRender)
|
||||
|
||||
// Set custom components if needed
|
||||
setCustomComponents('vitepress', {
|
||||
// Your custom components here
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore [Components documentation](/guide/vue2-components) for all available components
|
||||
- Check out [API Reference](/guide/api) for detailed API documentation
|
||||
- See [Examples](/guide/examples) for more usage examples
|
||||
@@ -0,0 +1,21 @@
|
||||
# Why use markstream-vue
|
||||
|
||||
- Progressive Mermaid: diagrams render incrementally so users see results earlier.
|
||||
- Streaming diff code blocks: show diffs as they arrive for instant feedback.
|
||||
- Built for scale: optimized DOM updates and memory use for very large documents.
|
||||
|
||||
This library is especially suited for real-time, AI-driven, and large-document scenarios where a conventional, static Markdown pipeline would cause lag or break UX.
|
||||
|
||||
Try this — test the renderer quickly in a browser component:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
|
||||
const md = '# Why test\n\nStreaming works well in large docs'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRender :content="md" />
|
||||
</template>
|
||||
```
|
||||
Reference in New Issue
Block a user