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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,91 @@
# E2E 测试报告(重定向)
该页面已迁移到中文指南目录:
- 新页面:`/zh/guide/e2e-testing-report`
如果你的链接仍指向 `/zh/e2e-testing-report`,页面将自动跳转。
<script>
if (typeof window !== 'undefined') {
window.location.replace('/zh/guide/e2e-testing-report')
}
</script>
<noscript>
页面已迁移:请访问 <a href="/zh/guide/e2e-testing-report">中文指南 – E2E 测试报告</a>
</noscript>
# E2E 测试报告(中文)
> 英文版参见:`docs/e2e-testing-report.md`。
本文档汇总了仓库中新增的端到端(E2E)Markdown 解析测试,便于在未来持续扩展覆盖范围。
## 目标
提供一个精简、可复现的 E2E 测试套件,用于验证解析器和渲染器是否能正确处理常见的 Markdown 结构,并作为后续扩展的基础。
## 新增内容
- `test/fixtures/` —— 收录具有代表性的 Markdown 用例:
- `headings.md`
- `code-diff.md`
- `table.md`
- `admonition.md`
- `math.md`
- `footnotes.md`
- `image-link.md`
- `checkbox.md`
- `mermaid.md`
- `test/e2e.markdown.test.ts` —— Vitest 测试文件,主要职责:
- 读取每个 fixture
- 使用 `parseMarkdownToStructure`(基于 `getMarkdown`)进行解析
- 序列化为最小稳定结构并针对每个 fixture 生成快照
- 含有一个针对行内数学/代码块渲染的断言
- 快照 —— 由 Vitest 生成,存放在默认快照目录。
- `.github/workflows/ci.yml` —— GitHub Actions 工作流,在 `push` 与 `pull_request` 时执行:
- 安装依赖
- 运行 `pnpm test`
- 运行 `pnpm typecheck`
- 运行 `pnpm lint`
## 本地运行方式
如需在本地运行测试(并在输出预期发生变化时更新快照):
```bash
pnpm test -- -u
```
仅运行测试(不更新快照):
```bash
pnpm test
```
类型检查与 ESLint:
```bash
pnpm typecheck
pnpm lint
```
## 建议 / 下一步
1. 扩展快照内容(例如加入 `children` 文本片段与节点 `meta`),提升对节点 payload 回归的捕捉能力。
2. 增加以下边界场景的 fixture 与断言:
- 未闭合的代码围栏
- 深度嵌套列表中混合标记
- 转义括号等特殊场景
- 流式代码围栏的异常输入
3. 为需要 DOM 或 Worker 的插件(Mermaid、KaTeX、Monaco)补充集成测试,并考虑在 CI 中加入基于 Playwright 的浏览器流程。
如果需要,我可以:
1. 立即扩充快照的元信息。
2. 补充上述边界场景的 fixture 与测试。
3. 起草一个面向 DOM 插件的 Playwright CI 任务。
欢迎告知下一步的优先项。
@@ -0,0 +1,158 @@
# 高级解析与低层自定义
这页面向更底层的解析定制:token 变换、AST 后处理,以及自定义节点管线。
如果你只是要完成这些常见任务,建议先去更直接的页面:
- 替换 `image`、`code_block`、`mermaid` 这类内置渲染器: [覆盖内置组件](/zh/guide/component-overrides)
- 支持 `thinking` 这类可信标签: [自定义标签与高级组件](/zh/guide/custom-components)
- 只想调现有行为,不想改解析器: [Props 与选项](/zh/guide/props)
## parseOptions
`parseOptions` 可传递给 `MarkdownRender` 或直接用于 `parseMarkdownToStructure`。
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — 在 `markdown-it` 解析后立即变换 tokens
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — 进一步变换 tokens
如需改造 AST,可在 `parseMarkdownToStructure` 返回后自行处理 `ParsedNode[]`,再通过 `MarkdownRender` 的 `nodes` 传入。
### 示例:自定义 HTML‑like 标签(推荐)
对于 `<thinking>...</thinking>` 这类简单自定义标签,现在不再需要先正则换行或重写 token。只要把标签加入白名单并注册组件即可:
```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"
/>
```
当 `custom-html-tags` 包含某个标签名时,解析器会:
- 在流式场景下吞并未闭合中间态,直到 `<tag ...>` 完整出现;
- 输出 `CustomComponentNode`(`type: 'thinking'`,即标签名本身,不含尖括号),并带上 `content`、可选 `attrs` 与 `loading/autoClosed` 标记。
## 自定义组件解析示例
上面的内置白名单 + 自定义节点管线已覆盖大多数“组件式”标签(内联或块级)。
当你需要进一步改造节点(例如剥掉包裹、合并分段、手动映射 attrs)时,再使用钩子:
```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 })
```
其他可选方案(按复杂度递增):
- 后端将 `thinking` 拆成单独字段/类型:前端用两次 `MarkdownRender`,一个渲染 `thinking`,另一个渲染剩余正文,互不干扰。
- 在进入解析前用正则占位:先 `replace` 掉 `<thinking>...</thinking>` 保存到数组;正文用 `MarkdownRender`;最后把占位符替换回自定义组件渲染结果。`thinking` 常在开头时可以直接截取头部做单独渲染。
## setCustomComponents(id, mapping)
- 使用 `setCustomComponents('docs', { thinking: ThinkingComponent })` 作用于带 `custom-id="docs"` 的 `MarkdownRender` 实例。
- 调用 `removeCustomComponents` 清理映射,避免单页应用内存泄漏。
## 作用域示例
```vue
<MarkdownRender content="..." custom-id="docs" />
// 在 setup 中
setCustomComponents('docs', { thinking: ThinkingNode })
```
高级钩子是为 Markdown 添加领域语法的强大方式,无需更改核心解析器。
### Typewriter 属性
`MarkdownRender` 支持 `typewriter` 布尔属性,控制非 `code_block` 节点的轻量 fade 表现。新节点仍然会走小型 enter 过渡,而流式过程中原地追加出来的那段新文本也会单独补一段短 fade,不再让整个节点容器一起变暗。适用于演示 UI,但在 SSR 或打印/导出场景下可能不需要。
示例:
```vue
<MarkdownRender :content="markdown" :typewriter="false" />
```
CSS 变量:`--typewriter-fade-duration` 和 `--typewriter-fade-ease` 用于首屏 enter 过渡;`--stream-update-fade-duration` 和 `--stream-update-fade-ease` 用于流式追加文本时那段短 fade。若不单独覆盖,流式追加的 fade 会默认沿用 `typewriter` 的时长和缓动。
## 国际化(i18n)
默认 `getMarkdown` 使用英文 UI 文案(如代码块复制按钮)。你可以通过 `i18n` 选项自定义这些文本:
**翻译映射用法:**
```ts
import { getMarkdown } from 'markstream-vue'
const md = getMarkdown('editor-1', {
i18n: {
'common.copy': '复制',
}
})
```
**翻译函数用法:**
```ts
import { getMarkdown } from 'markstream-vue'
import { useI18n } from 'vue-i18n' // 或其他 i18n 库
const { t } = useI18n()
const md = getMarkdown('editor-1', {
i18n: (key: string) => t(key)
})
```
**全局覆盖默认文案:**
如果你只是想修改一些默认 UI 文案(例如 "Copy" → "复制"),而不需要完整的 i18n 功能,可以使用 `setDefaultI18nMap` 来替换默认的翻译映射:
```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': '图片加载中...',
})
```
这个功能适合以下场景:
- 不需要语言切换,只是觉得默认文案不合适
- 希望默认使用中文(或其他语言)UI
- 没有使用 `vue-i18n`,但仍想自定义 UI 文案
**默认翻译键:**
- `common.copy`:代码块复制按钮文本
- `common.copied`:复制成功后的提示文本
- `image.loadError`:图片加载失败的提示
- `image.loading`:图片加载中的提示
该设计保证 markdown 工具函数纯净,无全局副作用,可与任意 i18n 方案集成或直接传入静态翻译。
@@ -0,0 +1,104 @@
---
description: 用 nodes 加 final 模式构建 AI 聊天与流式 Markdown 界面,并掌握性能、可信标签与 SSR 安全接入方式。
---
# AI 聊天与流式输出
当你在做聊天界面、token 流式输出、SSE 响应预览,或者任何“用户正在看着内容持续变化”的 Markdown 场景时,就走这条路径。
如果你的页面其实是静态文章、文档站或低频更新页面,请回到 [使用与流式渲染](/zh/guide/usage),优先使用更简单的 `content` 路径。
## 1. 先选最小安装组合
| 需求 | 安装包 | 适合场景 |
| --- | --- | --- |
| 纯文本或轻量聊天界面 | `markstream-vue` | 基础 Markdown、列表、链接、引用 |
| 不用 Monaco 的代码高亮 | `markstream-vue stream-markdown` | SSR 友好的聊天记录、较小 bundle |
| 更强的代码交互 | `markstream-vue stream-monaco` | 复制、预览、diff、Monaco 代码块 |
| 聊天内容里有图表或公式 | `markstream-vue mermaid katex` | Mermaid 图表和 KaTeX 公式 |
只安装你预期回复里真的会出现的能力,对聊天界面的收益通常很大。
## 2. 推荐的数据流
对于高频更新场景,把解析放到 `MarkdownRender` 外面,然后传 `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>
```
这样做的好处:
- 每次 token 更新时,不需要在 `MarkdownRender` 内部整篇重解析。
- 未来想把解析移到 store、worker 或消息流水线里时,不用改渲染器接法。
- `custom-id="chat"` 给了你一个安全的作用域,用来定制聊天界面样式或替换单个节点。
## 3. 这几个渲染配置通常最稳
- 长聊天记录优先保留默认虚拟化;只有在你有明确性能测量时再去调 `maxLiveNodes`。
- 如果代码块很多,但 Monaco 对当前聊天界面太重,可以先用 `renderCodeBlocksAsPre` 降级。
- 重型 peers 先别全装。聊天类页面最容易从“不默认带 Mermaid、KaTeX、Monaco”里拿到体积收益。
- 如果你关闭虚拟化(`:max-live-nodes="0"`),那 [Props 与选项](/zh/guide/props) 里的 batching 相关配置就会更重要。
## 4. 常见升级路径
### 更好的代码块
- 想要更轻的文档风格:用 `MarkdownCodeBlockNode`,配 `stream-markdown`
- 想要更强的预览 / diff / 交互:用 `CodeBlockNode`,配 `stream-monaco`
具体差异看 [渲染器与节点组件](/zh/guide/components)。
### `thinking` 这类可信标签
使用 `custom-html-tags` + `setCustomComponents('chat', mapping)`,让自定义标签只作用在聊天区域。
详见 [自定义标签与高级组件](/zh/guide/custom-components)。
### 只在一个消息区域里做覆盖
通过 `setCustomComponents('chat', { image: ChatImageNode })` 注册,再配合 `custom-id="chat"` 渲染。
详见 [覆盖内置组件](/zh/guide/component-overrides)。
## 5. CSS 与 SSR 检查清单
- 先引入 reset,再在 `@layer components` 中导入 `markstream-vue/index.css`
- 只有启用数学公式时,才额外导入 `katex/dist/katex.min.css`
- SSR 场景下,把 Mermaid、D2、Monaco 这类浏览器专属依赖放到 client-only 边界之后
- 如果样式串到别的区域,所有聊天界面的定制都收口到 `[data-custom-id="chat"]`
页面效果不对时,先从这里开始排: [故障排除](/zh/guide/troubleshooting#css-looks-wrong-start-here)
## 6. 什么时候不该走这条路径
- 更新频率不高、页面基本静态时,用 `content` 更简单
- 如果服务端或别的层已经接管 Markdown 解析,就直接用预解析后的 `nodes`
- 如果当前问题主要是 SSR / runtime 边界,而不是流式输出本身,优先看对应框架文档
## 下一步继续看
- [安装](/zh/guide/installation):选 peers
- [使用与流式渲染](/zh/guide/usage):理解 `content` vs `nodes`
- [性能](/zh/guide/performance):处理更长的聊天记录
- [渲染器与节点组件](/zh/guide/components):选择代码块 / 图表 / 公式组件
- [故障排除](/zh/guide/troubleshooting):排 CSS、peers 和 SSR
@@ -0,0 +1,178 @@
# AI / Skills 工作流
如果你会用 Codex、Cursor、Claude Code、ChatGPT 之类的编码助手来接入 markstream,这页的目标就是帮你把任务描述得更准确,少走弯路。
这里不是泛泛而谈,而是直接给你可复制的任务模板、接入清单,以及一个 markstream 相关 skill 至少该覆盖什么。
## 先按你的目标选入口
- 想直接安装可用的 skills:先看 [安装打包后的 skills](#安装打包后的-skills)
- 想直接复制官方 prompt 模板:先看 [可直接复制的任务模板](#2-可直接复制的任务模板)
- 想让 AI 帮你做迁移:先看 [可直接复制的任务模板](#2-可直接复制的任务模板),再配合文末的接入顺序
- 想把 AI 资产跟着仓库一起版本化:看 [仓库里的现成资产](#仓库里的现成资产)
## 安装打包后的 skills
对最终用户来说,最推荐的是直接使用通用的 `skills` 安装器:
```bash
npx skills add Simon-He95/markstream-vue
```
这条路径之所以可用,是因为仓库里的可复用 skills 现在已经放在 `.agents/skills` 下,`npx skills` 可以直接从 GitHub 仓库识别并安装。
Vercel 的 `skills` 安装器也支持多种来源格式:
```bash
# GitHub shorthand(owner/repo)
npx skills add Simon-He95/markstream-vue
# 完整 GitHub URL
npx skills add https://github.com/Simon-He95/markstream-vue
# 仓库里的单个 skill 直链
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
# 任意 git URL
npx skills add git@github.com:Simon-He95/markstream-vue.git
```
如果你更希望走包级 CLI,也仍然可以使用:
```bash
npx markstream-vue skills install
# 或
pnpm dlx markstream-vue skills install
```
常用变体还有:
```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` 是当前最推荐的 Codex-compatible 安装方式
- `skills install` 默认使用 `copy`,更适合 `npx` / `dlx` 场景
- 默认目标目录是 `~/.agents/skills`
- `--target codex` 会安装到 `${CODEX_HOME:-~/.codex}/skills`
- `--target` 可以传 `agents`、`codex`,也可以传任意自定义路径
- `--force` 用来替换目标目录里已有的安装
`prompts/` 仍然保留在仓库里;包级安装命令只处理 skills 目录。
同一个 CLI 也可以直接暴露 prompts:
```bash
npx markstream-vue prompts list
npx markstream-vue prompts show install-markstream
```
对于 npm 用户来说,这是发现和复用官方 prompt 模板最方便的方式,不需要先克隆仓库。
## 仓库里的现成资产
这个仓库现在已经有可版本化、可复用的资产:
- 任务型 skills:`markstream-install`、`markstream-custom-components`、`markstream-migration`
- 框架入口 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`
`.agents/skills/` 适合放可复用、并且能被 GitHub 安装器自动发现的 Codex 工作流资产。任务型 skills 负责跨框架工作流,框架入口 skills 则让 Vue 3、Nuxt、Vue 2、Vue 2 CLI、Vue 2 Vite、React、Angular 这些场景更容易被直接发现;`prompts/` 适合放给人类或其他助手直接复制使用的提示词模板。
## 1. 先把这五类信息告诉 AI
在让 AI 开始改代码前,尽量先说明:
- 框架和版本,例如 Vue 3、Nuxt 3、React 18、Angular 20
- CSS 技术栈,例如 Tailwind、UnoCSS、reset 库、设计系统
- 渲染模式:静态文章、文档站、SSR,还是流式聊天
- 需要哪些可选能力:Monaco、Mermaid、D2、KaTeX、Shiki
- 组件覆盖是否必须限制在某个业务区域内
少了这些上下文,AI 很容易装错 peer,或者把 CSS 放到错误的顺序里。
## 2. 可直接复制的任务模板
### 在现有 Vue 3 项目里安装 markstream
```text
把 markstream-vue 接入到这个 Vue 3 项目里。
只安装满足这些能力所需的最小 peer 依赖:[在这里填写 Monaco / Mermaid / D2 / KaTeX / Shiki]。
结合我现有的 CSS 技术栈处理样式顺序:[在这里填写 Tailwind / UnoCSS / reset 库]。
补一个最小可运行示例,并说明我应该用 `content` 还是 `nodes`。
如果需要覆盖组件,请默认使用带 `custom-id` 的 scoped 方式。
```
### 从 `react-markdown` 迁移
```text
把这个 React 项目从 react-markdown 迁移到 markstream-react。
先审计哪些 remark/rehype 行为没有直接 1:1 对应项。
优先迁移简单的渲染器替换,再标出哪些地方需要 custom nodes、customHtmlTags,或者解析后处理。
尽量保持现有用户可见行为不变。
```
### 支持 `thinking` 这类自定义标签
```text
为这个项目增加一个可信的 Markdown 标签 `thinking`。
请使用 `customHtmlTags` + scoped `setCustomComponents`。
自定义组件内部需要继续渲染嵌套 Markdown,并保持流式输出友好。
除非有充分理由,否则不要使用全局覆盖。
```
### 排查样式或 SSR 问题
```text
审计这套 markstream 接入的 CSS 顺序、reset 冲突、可选 peers 和 SSR 问题。
重点检查 `markstream-vue/index.css` 是否在正确的 layer 中、KaTeX CSS 是否缺失、浏览器专属依赖是否在 client-only 边界之后初始化。
最后给出最小修复集。
```
## 3. 一个可复用的 markstream skill 至少该做什么
如果你在做可复用的 prompt、模板或 coding skill,建议它稳定覆盖这些步骤:
- 自动识别框架和包管理器
- 按需求只安装必要的 peers
- 遇到 Tailwind / UnoCSS 时,把 CSS 放在 reset 之后,并放进 `@layer components`
- 普通渲染优先用 `content`,流式输出优先用 `nodes` + `final`
- 组件覆盖默认走 `custom-id` 作用域
- 改完后告诉用户下一步应该看哪一页文档
一个好的 skill 可以顺手引用这些页面:
- [安装](/zh/guide/installation)
- [使用与流式渲染](/zh/guide/usage)
- [覆盖内置组件](/zh/guide/component-overrides)
- [自定义标签与高级组件](/zh/guide/custom-components)
- [故障排除](/zh/guide/troubleshooting)
## 4. 让 AI 也读懂仓库上下文
如果你使用的是“能读仓库”的代理型助手,最好再让它一起参考:
- [AI / LLM 上下文](/llms.zh-CN)
`/llms.zh-CN` 更偏代理使用的项目地图;这页更偏人类可直接复制的任务模板。实际效果最好的是两者一起给:
- 这页的任务模板
- `/llms.zh-CN` 的仓库上下文
## 5. 一个很稳的接入顺序
当任务不只是“一页 demo”时,让 AI 按这个顺序推进,成功率通常最高:
1. 安装并确认 peers
2. 先让基础渲染器跑通
3. 修正 CSS 顺序
4. 再增加 scoped 覆盖或自定义标签
5. 最后跑测试或文档构建
这样能明显减少“代码能编译,但页面效果不对”的情况。
@@ -0,0 +1,66 @@
# Angular 安装
在你的 Angular 20+ 项目中安装 `markstream-angular`。
```bash
pnpm add markstream-angular @angular/core @angular/common
```
## 必需样式
在应用入口中导入一次渲染器样式:
```ts
import 'markstream-angular/index.css'
```
如果启用了数学公式渲染,还需要:
```ts
import 'katex/dist/katex.min.css'
```
## 可选对等依赖
只安装你需要的重功能:
| 功能 | 包 |
| --- | --- |
| Monaco 代码块 | `stream-monaco` |
| Mermaid 图表 | `mermaid` |
| KaTeX 数学公式 | `katex` |
| D2 图表 | `@terrastruct/d2` |
| AntV infographic block | `@antv/infographic` |
一次性安装:
```bash
pnpm add stream-monaco mermaid katex @terrastruct/d2 @antv/infographic
```
## 可选 Worker
`markstream-angular` 也导出了和 React / Vue 集成一致的 Worker 入口:
```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())
```
## 本地 Playground
在这个 monorepo 里,Angular playground 有这些路由:
- `/` 主 streaming demo
- `/test` 回归实验室
- `/test-sandbox` 独立 framework/version 对照页
本地运行:
```bash
pnpm play:angular
```
@@ -0,0 +1,108 @@
# Angular 快速开始
在 Angular standalone 应用里快速跑起 `markstream-angular`。
## 基础示例
```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
这是 **markstream-angular**。
\`\`\`ts
export function greet(name: string) {
return \`hello \${name}\`
}
\`\`\`
\`\`\`mermaid
flowchart LR
输入 --> 解析 --> 渲染
\`\`\`
$E = mc^2$
`)
}
bootstrapApplication(AppComponent)
```
## 流式输出示例
```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,
}
```
## 自定义标签
Angular 版也支持 playground 里 `thinking` 这类 custom HTML tag 流程:
```html
<markstream-angular
[content]="markdown()"
[customHtmlTags]="['thinking']"
[customComponents]="customComponents"
/>
```
## 下一步
- [Angular 安装](/zh/guide/angular-installation)
- [指南首页](/zh/guide/)
- [Playground 指南](/zh/guide/playground)
@@ -0,0 +1,116 @@
---
description: 查阅 markstream-vue 的底层 API,包括解析器工具、渲染流程选择、作用域覆盖与 AST 级接入能力。
---
# API 参考
本页聚焦 `markstream-vue` 的底层入口:解析器工具、渲染流程选择,以及作用域覆盖相关钩子。
如果你是想查 `MarkdownRender`、`CodeBlockNode`、`ImageNode` 这类导出的 Vue 组件,请优先看 [渲染器与节点组件](/zh/guide/components)。搭配 [使用示例](/zh/guide/usage) 与 [props](/zh/guide/props) 页面一起阅读效果更佳。
## 渲染流程速览
```
Markdown 字符串 → getMarkdown() → markdown-it-ts 实例
↓
parseMarkdownToStructure(content, md) → AST (BaseNode[])
↓
<MarkdownRender> → 节点组件(CodeBlockNode、ImageNode 等)
```
可在任意阶段介入:
- 直接传 `content`:组件自动解析。
- 传 `nodes`:自己在服务端/预处理阶段生成 AST 并复用。
## 解析器工具
| Helper | 作用 | 适用场景 |
| ------ | ---- | -------- |
| `getMarkdown(msgId?, options?)` | 返回预配置的 `markdown-it-ts` 实例。 | 需调整 parser 选项(HTML、插件)或复用实例时。 |
| `parseMarkdownToStructure(content, md)` | 生成渲染器使用的 AST。 | 服务端预解析、静态导出、或需在渲染前做校验时。 |
两者均可在 Node/浏览器使用。处理大文档时可复用 `md` 实例避免重复初始化插件。
## 自定义组件与作用域
通过 `setCustomComponents(customId?, mapping)` 覆盖任意节点渲染器,再在 `MarkdownRender` 上传入匹配的 `custom-id`,即可限定覆盖范围。
```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 = '![demo](https://example.com/demo.png)'
</script>
<template>
<MarkdownRender custom-id="docs" :content="md" />
</template>
```
提示:
- 使用语义化 ID(如 `docs`、`playground`)方便排查。
- `setCustomComponents(mapping)` 会注册全局映射;更推荐按 ID 作用域隔离。
- 在 SPA 中按需注册/卸载时,记得在路由切换时清理。
## 解析钩子与节点变换
当使用 `content` 时,可通过 `parse-options`(组件 prop)或 `parseMarkdownToStructure` 的 `ParseOptions` 拦截解析阶段:
- `preTransformTokens(tokens)` — 生成节点前预处理 token。
- `postTransformTokens(tokens)` — 在默认处理后继续调整。
如需改造 AST,可在 `parseMarkdownToStructure` 返回后自行处理,再通过 `MarkdownRender` 的 `nodes` 传入。
示例:把 AI “thinking” 标签直接渲染成自定义组件(无需钩子)
```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>
```
如果你需要进一步改造 `thinking` 节点(剥掉包裹、重映射 attrs、合并分段等),可使用上述 hooks,或在解析后自行处理 AST。
## 其他导出
- 节点组件:`CodeBlockNode`、`MarkdownCodeBlockNode`、`MermaidBlockNode`、`MathBlockNode`、`ImageNode` 等(详见 [组件与节点渲染器](/zh/guide/components))。
- 工具:`VueRendererMarkdown`(全局组件插件)与共享类型定义(组件 props/解析器类型;参考 [/zh/guide/parser-api](/zh/guide/parser-api) 或 npm 上的 `stream-markdown-parser` README)。
## 样式 & 排障提醒
- 先引入 reset,再在 `@layer components` 导入 `markstream-vue/index.css`,防止 Tailwind/UnoCSS 覆盖。参考 [Tailwind 指南](/zh/guide/tailwind)。
- 同伴依赖中,KaTeX 需要自己的 CSS;Mermaid 不需要。缺失 KaTeX 样式时通常表现为空白公式。
- 使用 `custom-id` + `[data-custom-id="docs"]` 来局部覆盖样式。
- 遇到样式异常时,依照 [排查清单](/zh/guide/troubleshooting#css-looks-wrong-start-here) 逐项检查。
需要更多示例?打开 [Playground](/zh/guide/playground) 或运行 `pnpm play` 在本地实验解析/渲染组合。
@@ -0,0 +1,268 @@
# CodeBlockNode 组件
`CodeBlockNode` 是库中用于渲染富交互代码块的组件。对于需要编辑/高亮/增量渲染的场景,推荐安装 `stream-monaco`。组件为头部提供灵活的自定义点(props + slots),并在常见场景下提供可拦截的事件。
## 快速概览
- Monaco 模式(安装 `stream-monaco`)— 类编辑器渲染,带 worker 支持
- 降级模式 — 未安装 `stream-monaco` 时会回退为纯 `<pre><code>` 渲染
- 如果你希望用 Shiki(不引入 Monaco),请使用 `MarkdownCodeBlockNode`(同伴依赖:`stream-markdown`)
## Props
完整签名请参阅 `src/types/component-props.ts`。关键 props:
- `node` — code_block 节点(必需)
- `loading`、`stream`、`isShowPreview`
- `monacoOptions` — 类型为 `CodeBlockMonacoOptions`,会透传给 `stream-monaco`
- `diffHideUnchangedRegions`、`diffLineStyle`、`diffAppearance`、`diffUnchangedRegionStyle`、`diffHunkActionsOnHover`、`diffHunkHoverHideDelayMs`、`onDiffHunkAction` 这类 diff 配置都应该放这里
- 头部控制:`showHeader`、`showCollapseButton`、`showCopyButton`、`showExpandButton`、`showPreviewButton`、`showFontSizeButtons`、`showTooltips`
Monaco diff 模式下的默认行为:
- `diffHideUnchangedRegions: { enabled: true, contextLineCount: 2, minimumLineCount: 4, revealLineCount: 5 }`
- `diffLineStyle: 'background'`
- `diffAppearance: 'auto'`
- `diffUnchangedRegionStyle: 'line-info'`
- `diffHunkActionsOnHover: true`
- `diffHunkHoverHideDelayMs: 160`
你可以通过 `monacoOptions` 覆盖这些默认值。
当 preset 使用 `diffAppearance: 'auto'` 时,`CodeBlockNode` 会先根据当前明暗外观解析成实际的 light/dark,再传给 `stream-monaco`。
Diff 代码块的内置 header 现在也会显示 `- / +` 行数统计。
## Slots 插槽
- `header-left` — 替换左侧头部
- `header-right` — 替换右侧头部
- `loading` — 自定义流式禁用时的占位符
## Emits 事件
- `copy(text: string)` — 点击复制时触发
- `previewCode(payload)` — 仅在你监听 `@preview-code` 时才会触发;payload 为 `{ node, artifactType, artifactTitle, id }`
## 示例
### 安装并运行(Monaco 模式)
```bash
pnpm add stream-monaco
```
### 基础示例
```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>
```
### 替换头部并隐藏复制按钮
```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">
自定义左侧
</div>
</template>
<template #header-right>
<button @click="runSnippet">
运行
</button>
</template>
</CodeBlockNode>
</template>
```
### 自定义加载占位符
```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">
正在加载编辑器资源…
</div>
</template>
</CodeBlockNode>
</template>
```
## 主题切换
`CodeBlockNode` 支持基于深色/浅色模式的自动主题切换。使用 `@vueuse/core` 的 `useDark` composable 来追踪主题状态,并将主题名称传递给 `MarkdownRender` 或 `CodeBlockNode`。
### 在独立的 Vue 应用中使用 @vueuse/core
```vue
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import MarkdownRender from 'markstream-vue'
const isDark = useDark() // Ref<boolean> 响应式系统/主题偏好
const toggleDark = useToggle(isDark)
const content = '# 示例\n\n```js\nconsole.log("深色模式")\n```'
// 可用主题(必须包含你想要使用的主题)
const themes = [
'vitesse-dark',
'vitesse-light',
'github-dark',
'github-light',
// ... 更多主题
]
</script>
<template>
<div>
<button @click="toggleDark()">
切换主题
</button>
<MarkdownRender
:is-dark="isDark"
code-block-dark-theme="vitesse-dark"
code-block-light-theme="vitesse-light"
:themes="themes"
:content="content"
/>
</div>
</template>
```
### VitePress 集成
对于 VitePress,使用 VitePress 内置的 `useData()` 中的 `isDark`:
```ts
// docs/.vitepress/theme/composables/useDark.ts
import { useData } from 'vitepress'
/**
* VitePress 主题 composable 用于深色模式
* 使用 VitePress 内置的 useData() 获取 isDark
*/
export function useDark() {
const { isDark } = useData()
return isDark
}
```
```vue
<!-- 在任意 .md 文件或组件中 -->
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { useDark } from '../../.vitepress/theme'
const isDark = useDark()
const content = '# 示例\n\n```js\nconsole.log("深色模式")\n```'
const themes = [
'vitesse-dark',
'vitesse-light',
'github-dark',
'github-light',
// ... 更多主题
]
</script>
<template>
<MarkdownRender
:is-dark="isDark"
code-block-dark-theme="vitesse-dark"
code-block-light-theme="vitesse-light"
:themes="themes"
:content="content"
/>
</template>
```
**工作原理:**
`theme` prop 支持固定主题或明暗配对:
```vue
<!-- 自动切换明暗(推荐) -->
<CodeBlockNode :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" />
<!-- 固定主题(忽略 isDark) -->
<CodeBlockNode theme="monokai" />
<!-- 主题对象(固定,忽略 isDark) -->
<CodeBlockNode :theme="{ name: 'my-theme', colors: { ... } }" />
```
使用 `{ light, dark }` 配对时,组件根据 `isDark` prop 自动切换。
`themes` prop 用于注册可用主题,以便 Monaco 可以按需懒加载它们。
> **向后兼容:** `darkTheme` / `lightTheme` props 仍然可用但已废弃。推荐使用统一的 `theme` prop。
**CodeBlockNode 的关键差异:**
| Prop | 直接使用 CodeBlockNode | 通过 MarkdownRender |
|------|---------------------|-------------------|
| `isDark` | 直接传给 `<CodeBlockNode :is-dark="isDark" />` | 通过 `<MarkdownRender :is-dark="isDark" />` 传入并自动转发 |
| 主题 | `:theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }"` | `:code-block-dark-theme="'vitesse-dark'"` `:code-block-light-theme="'vitesse-light'"` (兼容) |
| 主题列表 | `:themes="['vitesse-dark', 'vitesse-light', ...]"` | `:themes="['vitesse-dark', 'vitesse-light', ...]"` |
## 注意事项
- CodeBlock 头部 API 在 [codeblock-header](/zh/guide/codeblock-header) 中有文档说明(包含替换头部和自定义加载占位符的示例)。
- `CodeBlockNode` 和 `MermaidBlockNode` 的 `copy` 事件 payload 不同:`CodeBlockNode` 触发 `copy(text: string)`,而 `MermaidBlockNode` 触发 `copy(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)`(支持 `preventDefault()`)。
快速尝试 — 简单的行内用法示例:
```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>
```
@@ -0,0 +1,98 @@
# 代码块渲染
## 概述
代码块渲染有三种策略,取决于你安装的可选依赖与配置:
- Monaco(推荐,用于大型/交互式代码块):安装并使用 `stream-monaco`,提供类似编辑器的流式增量渲染体验。库在运行时按需懒加载 `stream-monaco`。
- Shiki(MarkdownCodeBlockNode):安装 `stream-markdown`,并通过 `setCustomComponents` 覆盖 `code_block` 节点以使用轻量的 Markdown 渲染器。
- 回退(无额外依赖):如果两个可选包均未安装,代码块会退回为普通的 `<pre><code>` 渲染(仅基础样式 / 无 Monaco 功能)。
## Monaco(推荐)
- 安装:
```bash
pnpm add stream-monaco
# or
npm i stream-monaco
```
- 行为:当 `stream-monaco` 可用时,内置的 `CodeBlockNode` 会使用基于 Monaco 的流式更新,适合大型或频繁更新的代码块。
- Vite Worker 注意事项:Monaco 与部分基于 Worker 的功能需要在打包时正确配置 Worker(例如 Vite 的 worker 配置),以确保运行时能加载对应的 worker。有关配置示例与 SSR 安全初始化,请参阅 [/zh/nuxt-ssr](/zh/nuxt-ssr)。
- 另请参阅:[/zh/guide/monaco](/zh/guide/monaco),其中包含 worker 打包建议与预加载示例。
## Shiki 模式(MarkdownCodeBlockNode)
- 安装:
```bash
pnpm add stream-markdown
# or
npm i stream-markdown
```
- 通过 `setCustomComponents` 覆盖 `code_block` 节点以注册 Shiki 版渲染器。示例:
```ts twoslash
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
setCustomComponents({ code_block: MarkdownCodeBlockNode })
```
设置后,`code_block` 会使用 `MarkdownCodeBlockNode`(由 `stream-markdown` + Shiki 驱动)。你也可以自定义组件并直接使用 `stream-markdown`。
### 语言图标懒加载
为了减小主包体积,低频语言图标已拆分到异步 chunk:
- 常见语言(JS/TS/HTML/CSS/JSON/Python 等)图标仍在主包内。
- 低频语言图标按需加载,异步 chunk 返回后会自动刷新图标显示。
- 如果你希望避免首次命中时的回退图标,可在应用空闲阶段预热一次:
```ts twoslash
import { preloadExtendedLanguageIcons } from 'markstream-vue'
if (typeof window !== 'undefined')
void preloadExtendedLanguageIcons()
```
快速试一下:
```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>
```
### Vue CLI 4(Webpack 4)注意事项
如果你使用 Vue CLI 4(Webpack 4),更推荐把代码块切到 Shiki 模式,并通过覆盖 `code_block` 来避免 Monaco 在 legacy bundler 下的一些兼容性问题。
踩坑与解决(可直接参考 `playground-vue2-cli`):
- Webpack 4 不支持 `package.json#exports`:建议通过 `resolve.alias` 指向真实的 `dist/*` 文件路径。
- `stream-markdown` 属于 ESM-only 包,在 `vue.config.js`(CJS)里可能无法用 `require.resolve('stream-markdown')` 找到:需要用文件系统兜底去定位 `node_modules/stream-markdown`,并 alias 到 `dist/index.js`。
- 如果你用 `IgnorePlugin` 忽略可选依赖,注意不要误伤 `stream-markdown`,否则运行时会出现 `webpackMissingModule`(表现为 “Cannot find module 'stream-markdown'”)。
## 回退
若未安装上述任一可选包,渲染器会回退为简单的 `pre`/`code` 表现。
## 参考链接
- Worker / SSR 指南:[/zh/nuxt-ssr](/zh/nuxt-ssr)
- 安装说明:[/zh/guide/installation](/zh/guide/installation)
@@ -0,0 +1,127 @@
# 代码块头部自定义
可以通过 props 控制代码块头部的显示,以及通过命名插槽替换头部左/右两侧内容。
常用 props:
- `showHeader` — 是否显示头部
- `showCollapseButton` — 头部折叠按钮
- `showCopyButton` — 复制按钮
- `showExpandButton` — 展开/收起按钮
- `showPreviewButton` — 预览按钮
- `showFontSizeButtons` — 字号控制按钮组
排查问题
如果遇到样式冲突(例如 Tailwind 的 reset 或 util 导致头部布局变化),请查看主排查页面以获取 Tailwind 导入顺序和解决方法:`/zh/guide/troubleshooting`。
示例
下面是常用的示例(README 中也包含同样的示例);这些示例通常可以直接在项目中复用。
隐藏头部(简单用法)
```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>
```
使用 `#header-left` / `#header-right` 插槽替换默认头部
```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>
```
自定义加载占位符(当 `stream=false` 且 `loading=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>
```
事件与注意事项
- 组件在复制操作时会触发 `copy` 事件(事件回调接收复制的文本);使用 `@copy="onCopy"` 监听。
- 组件仅在你监听 `@preview-code` 时才会触发 `previewCode` 事件;负载为 `{ node, artifactType, artifactTitle, id }`。(不监听时,内置预览弹窗仅支持 HTML。)
- 若要隐藏某类默认按钮,可使用布尔 props:`showCollapseButton`、`showCopyButton`、`showExpandButton`、`showPreviewButton`、`showFontSizeButtons`。
- 可通过 `showTooltips` 控制当前代码块头部按钮 tooltip 的显示。
- `showHeader` 控制是否渲染头部栏。
@@ -0,0 +1,8 @@
# 对比
将 `markstream-vue` 与常用选项进行对比:
- 与纯静态渲染相比,`markstream-vue` 支持流式渲染与逐步更新,适用于实时编辑场景。
- 与大型 Markdown-to-HTML 框架相比,目标是更小的内存占用与更快的首次渲染(以避免大量 DOM 操作)。
(更多细节参见英文版 `/guide/compared` 或中文版 `/zh/guide/compared`)
@@ -0,0 +1,185 @@
---
description: 用带作用域的方式覆盖 markstream-vue 的 image、code_block、mermaid、link 等内置组件,避免污染全局渲染。
---
# 覆盖内置组件
大多数高级定制,其实都应该先从 `setCustomComponents` 开始,而不是一上来就改解析器。
这页适合解决这些问题:
- 想替换 `image`、`code_block`、`mermaid`、`link` 等内置渲染器
- 想把覆盖范围限制在某一个业务区域
- 想弄清楚“我该用哪个 override key”
如果你想支持 `thinking` 这类可信自定义标签,请看 [自定义标签与高级组件](/zh/guide/custom-components)。如果你只是想调工具栏、性能或主题,优先看 [Props 与选项](/zh/guide/props)。
## 1. 默认用带作用域的覆盖
在渲染器上加 `custom-id`,再用相同的 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 = '![demo](https://example.com/demo.png)'
</script>
<template>
<MarkdownRender custom-id="docs" :content="markdown" />
</template>
```
这样做的好处:
- 不会让一个页面的覆盖误伤另一个页面
- 更容易测试和回收
- 更符合渲染器内部对 scoped / global mapping 的合并方式
单参数形式 `setCustomComponents({ ... })` 仍然可用,但更适合视作兼容性的全局覆盖写法。
## 2. 最常用的 override key
| Key | 替换的节点 | 说明 |
|-----|-----------|------|
| `image` | `ImageNode` | 最适合拿来做 lightbox、caption、懒加载封装 |
| `link` | `LinkNode` | 适合埋点、路由跳转、自定义 tooltip |
| `code_block` | 普通 fenced code block | 作为通用兜底使用,优先级在精确语言 key 和内置 `mermaid` / `d2` / `infographic` 路由之后 |
| `mermaid` | Mermaid fenced block | 只改 Mermaid 时优先用它,而不是改全部代码块 |
| `d2` | D2 / `d2lang` fenced block | 与 Mermaid 同理 |
| `infographic` | Infographic fenced block | 只影响 infographic 渲染 |
| `inline_code` | 行内代码 | 适合做文档排版或特殊 inline 行为 |
| `heading`、`paragraph`、`list_item`、`blockquote` | 容器节点 | 能力很强,但你需要自己处理 children |
完整节点列表可以看 [组件 API](/zh/guide/components) 和导出的 `CustomComponents` 类型。
## 3. 先从叶子节点开始
叶子节点最容易上手,因为不需要你额外把子节点再渲染一遍。
### 示例:替换 `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,
})
```
### 示例:把普通代码块切到 `MarkdownCodeBlockNode`
```ts twoslash
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
setCustomComponents('docs', {
code_block: MarkdownCodeBlockNode,
})
```
这只会影响普通代码块。Mermaid、D2 和 infographic 仍然会走各自的专用渲染器;如果你要覆盖它们,请分别使用 `mermaid`、`d2`、`infographic`。
### 示例:只接管一种 fenced language
```ts twoslash
import type { Component } from 'vue'
import { setCustomComponents } from 'markstream-vue'
declare const EChartsBlockNode: Component
setCustomComponents('docs', {
echarts: EChartsBlockNode,
})
```
这只会命中 language 为 `echarts` 的 fence。代码块的路由优先级是:
- 精确语言 key,比如 `echarts`
- 内置专用路由,比如 `mermaid`、`d2`、`infographic`
- 通用 `code_block`
### 示例:只覆盖 Mermaid
```ts twoslash
import type { Component } from 'vue'
import { setCustomComponents } from 'markstream-vue'
declare const CustomMermaidBlock: Component
setCustomComponents('docs', {
mermaid: CustomMermaidBlock,
})
```
当你只想改导出按钮、工具栏、品牌外壳,而不想影响所有代码块时,这就是最稳妥的方式。
## 4. 自定义渲染器会收到什么 props
每个自定义渲染器都会拿到这些基础 props:
- `node`
- `loading`
- `indexKey`
- `customId`
- `isDark`
另外,渲染器还会按节点类型继续透传一些能力相关 props:
- 普通代码块会拿到 `stream`、`theme`、`darkTheme`、`lightTheme`、`monacoOptions`、`themes`、`minWidth`、`maxWidth`,以及你传给 `codeBlockProps` 的内容(新的推荐字段是 `theme`;`darkTheme` / `lightTheme` 继续保留作兼容)
- Mermaid、D2、Infographic 会分别拿到对应的 `mermaidProps`、`d2Props`、`infographicProps`
- link 和 list 会继承 tooltip / typewriter 相关的上层配置
如果你想保留类型提示,优先使用 `markstream-vue` 导出的 props 类型。
## 5. 覆盖容器节点要更谨慎
`heading`、`paragraph`、`list_item`、`blockquote` 这类节点当然可以覆盖,但它们带有 children,你的组件需要自己决定如何继续渲染这些子节点。
实用建议:
- 除非你确实需要容器级控制,否则优先从 `image`、`link`、`code_block`、`mermaid`、`inline_code` 这类节点开始
- 如果必须覆盖容器节点,先看仓库里的内置实现 `src/components/*`,对照着补上 children 的渲染逻辑
- 始终用 `custom-id` 做作用域隔离,这样调试成本最低
## 6. 动态页面记得清理
如果某个 override 只在临时页面里使用,离开页面后可以主动移除:
```ts twoslash
import { removeCustomComponents } from 'markstream-vue'
removeCustomComponents('docs')
```
这在 SPA、Storybook、playground 和测试场景里尤其有帮助。
@@ -0,0 +1,319 @@
---
description: 快速查阅 MarkdownRender、CodeBlockNode、MermaidBlockNode、ImageNode、LinkNode 等 markstream-vue 导出组件。
---
# 渲染器与节点组件
当你已经明确知道自己要找的是某个导出的组件,并且想快速确认它的 props、事件、CSS 要求,以及该去哪个页面继续往下看时,就从这页开始。
如果你还在判断应该改解析流程、作用域覆盖,还是组件替换,建议先看:
- 想查解析器工具、AST hooks 或 `setCustomComponents()` 的接入方式: [API 参考](/zh/guide/api)
- 想有作用域地替换内置渲染器: [覆盖内置组件](/zh/guide/component-overrides)
- 想支持 `thinking` 这类可信标签: [自定义标签与高级组件](/zh/guide/custom-components)
- 想调配置、性能或工具栏: [Props 与选项](/zh/guide/props)
## 快速参考
| 组件 | 推荐场景 | 关键 props / 事件 | 额外 CSS / 同伴依赖 | 排障提示 |
| ---- | -------- | ---------------- | ------------------- | -------- |
| `MarkdownRender` | 渲染完整 AST(默认导出) | Props:`content` / `nodes`、`custom-id`、`final`、`parse-options`、`custom-html-tags`、`is-dark`、`code-block-props`、`mermaid-props`、`d2-props`、`infographic-props`;事件:`copy`、`handleArtifactClick`、`click`、`mouseover`、`mouseout` | 在 reset 之后引入 `markstream-vue/index.css`(CSS 已被限定在内部 `.markstream-vue` 容器中),并放入受控 layer | 用 `setCustomComponents(customId, mapping)` + `custom-id` 限定覆盖范围;配合 [CSS 排查清单](/zh/guide/troubleshooting#css-looks-wrong-start-here) |
| `CodeBlockNode` | 基于 Monaco 的交互式代码块、流式 diff | `node`、`monacoOptions`、`stream`、`loading`;事件:`copy`、`previewCode`;插槽 `header-left` / `header-right`;diff 悬浮操作配置放在 `monacoOptions`(`diffHunkActionsOnHover`、`diffHunkHoverHideDelayMs`、`onDiffHunkAction`) | 安装 `stream-monaco`(peer)并打包 Monaco workers | SSR 首包会先给 `<pre><code>` fallback;编辑器空白时优先检查 worker 打包和客户端增强链路 |
| `MarkdownCodeBlockNode` | 轻量级高亮(Shiki) | `node`、`stream`、`loading`;插槽 `header-left` / `header-right` | 同伴依赖 `stream-markdown` | SSR/低体积场景优先使用 |
| `MermaidBlockNode` | 渐进式 Mermaid 图 | `node`、`isDark`、`isStrict`、`maxHeight`、`estimatedPreviewHeightPx`;事件 `copy`、`export`、`openModal`、`toggleMode` | `mermaid` >= 11;无需额外 CSS | SSR 首包先给可读 fallback;异步渲染问题详见 `/zh/guide/mermaid` |
| `D2BlockNode` | 渐进式 D2 图 | `node`、`isDark`、`maxHeight`、`progressiveRender`、`progressiveIntervalMs`;工具栏开关 | `@terrastruct/d2`;无需额外 CSS | SSR 首包先给 fallback / 源码;缺少依赖时保持 fallback;详见 `/zh/guide/d2` |
| `MathBlockNode` / `MathInlineNode` | KaTeX 公式 | `node` | 安装 `katex` 并引入 `katex/dist/katex.min.css` | 注册同步 KaTeX loader 后可直接 SSR 出 HTML;否则稳定回退为原文 |
| `ImageNode` | 自定义图片预览 / 懒加载 | Props:`fallback-src`、`show-caption`、`lazy`、`svg-min-height`、`use-placeholder`;事件:`click` / `load` / `error` | 无额外 CSS | 通过 `setCustomComponents` 包装,实现 lightbox |
| `LinkNode` | 下划线动画、颜色自定义 | `color`、`underlineHeight`、`showTooltip` | 无 | 浏览器默认 `a` 样式可通过 reset 解决 |
| `VmrContainerNode` | 自定义 `:::` 容器 | `node`(`name`、`attrs`、`loading`、`children`) | 极简基础 CSS;通过 `setCustomComponents` 覆盖 | JSON attrs 会规范到 `node.attrs`(去掉 `data-` 前缀);无效/不完整 JSON 存到 `attrs.attrs`;name 后面的 args 存到 `attrs.args` |
## TypeScript 类型导出
`markstream-vue` 同步导出渲染器与组件 props 类型:
```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 每一行点号后面的字段名。
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
```
说明:
- `InstanceType<typeof MarkdownRender>['$props']` 是最直接的组件 props 查看入口。
- `NodeRendererProps` 是同一套公开 props 结构的命名类型导出。
- `codeBlockProps` 现在会跟随公开的 `CodeBlockNode` props 结构(去掉 `node`),因此像 `showHeader`、`showFontSizeButtons`、`showTooltips` 这类字段也能直接获得 hover 与补全。
- 新接入更推荐使用 `codeBlockProps.theme`;`darkTheme` / `lightTheme` 仍保留作兼容字段。
- 更推荐 hover 上面每一行点号后面的字段名,而不是只 hover 导入的类型名。
- 如果你主要想看组件 props 的 hover,优先看下面这段 `MarkdownRender` 示例。
- 只有写成 `ts twoslash` / `vue twoslash` 的代码块才会在这个文档站里显示 hover 类型信息。
语言图标默认使用内置 `material` theme。进阶接入可以通过导出的 helper 查看或切换 icon theme:
```ts
import { getRegisteredThemes, registerIconTheme, setIconTheme } from 'markstream-vue'
console.log(getRegisteredThemes()) // ['material']
setIconTheme('material')
// registerIconTheme(...) 可用于先注册你自己的图标包,再切换过去。
```
## 先快速判断该用哪个组件
- 大多数业务接入都应该先用 `MarkdownRender`。
- 如果你要自己拼接底层节点渲染器,再考虑 `CodeBlockNode`、`MermaidBlockNode`、`D2BlockNode`、`MathBlockNode`。
- 如果你只是想替换单个内置节点,通常看 `ImageNode`、`LinkNode`、`VmrContainerNode` 这一层就够了。
- 如果你脱离 `MarkdownRender` 单独渲染节点组件,请记得包一层 `<div class="markstream-vue">...</div>`,否则库内变量和样式不会完整生效。
## MarkdownRender
> 主入口:接受 Markdown 字符串或解析后的 AST,然后使用内置节点渲染器输出。
### 快速要点
- **适用**:Vite/Nuxt/VitePress 中渲染整篇 Markdown。
- **关键 props**:`content` / `nodes`、`custom-id`、`final`、`parse-options`、`custom-html-tags`
- **CSS 顺序**:先引入 reset,再在 `@layer components` 中导入 `markstream-vue/index.css`。
### CSS 作用域
`markstream-vue` 已把打包后的 CSS 限定在内部 `.markstream-vue` 容器中,用于降低全局样式冲突。
- 使用 `MarkdownRender` 时一般无需处理:它默认渲染在容器内部。
- 如果你独立使用节点组件(例如 `CodeBlockNode`、`MathBlockNode`),请外层包一层 `<div class="markstream-vue">...</div>`,这样库内样式与变量才会生效。
### 最适合先 hover 的目标
如果你只是想先看组件 props,先 hover 下面这段里的 `:content`、`custom-id`、`:is-dark`、`:code-block-monaco-options`:
```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>
```
### 使用阶梯
```vue twoslash
<script setup lang="ts">
import type { CodeBlockMonacoOptions } from 'markstream-vue'
import MarkdownRender from 'markstream-vue'
const md = '# 你好\n\n使用 custom-id 控制样式。'
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
// 注册自定义节点
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;
}
```
### 性能相关 props
- **批量渲染** —— `batchRendering`、`initialRenderBatchSize`、`renderBatchSize`、`renderBatchDelay`、`renderBatchBudgetMs` 控制每一帧有多少节点从占位骨架切换为真实组件。仅在关闭虚拟化(`:max-live-nodes="0"`)时会启用增量骨架模式。
- **延迟可见节点** —— `deferNodesUntilVisible` 与 `viewportPriority` 默认开启,让 Mermaid、D2、Monaco、KaTeX 等重型节点只有在接近视口时才加载。
- **虚拟化窗口** —— `maxLiveNodes` 限制 DOM 中最多保留多少个已渲染节点,`liveNodeBuffer` 控制超前/超后范围。详见 [性能指南](/zh/guide/performance)。
- **代码块降级** —— 通过 `renderCodeBlocksAsPre` 与 `codeBlockStream` 可将普通代码块切换为 `<pre><code>` 或关闭流式更新。
### 常见问题
- **样式错乱**:先检查 [CSS 排查清单](/zh/guide/troubleshooting#css-looks-wrong-start-here)。
- **工具类覆盖**:传入 `custom-id` 并使用 `[data-custom-id="docs"]` 限定样式。
- **SSR 报错**:渲染器本身已经 SSR-safe;只把你自己的浏览器专属页面逻辑,或手动初始化的 peer,放到 `<ClientOnly>` / `onMounted` 后面。
### 什么时候优先用它
- 你想走“默认解析器 + 默认渲染器”的最短接入路径。
- 你希望把流式渲染、虚拟化、批量渲染、自定义标签、作用域覆盖都集中在一个入口处理。
- 你还不想自己拼装各个节点渲染器,只是要在 `content` 与 `nodes` 之间做选择。
### 最常见的组合方式
- `content` + `custom-id`:静态页面或轻量定制页面。
- `nodes` + `final`:流式输出、服务端预解析、增量渲染。
- `custom-html-tags` + `setCustomComponents(customId, mapping)`:像 `thinking` 这样的可信标签。
- `renderCodeBlocksAsPre`:SSR 或受限环境里,把重型代码块降级成普通 `<pre><code>`。
## CodeBlockNode
> 基于 Monaco 的代码块渲染器,支持流式 diff 和交互式工具栏。
- **适合**:代码审阅、diff 检查、补丁预览、悬浮操作
- **关键 props**:`node`、`monacoOptions`、`stream`、`loading`
- **事件**:`copy`、`previewCode`
- **插槽**:`header-left`、`header-right`
- **同伴依赖**:`stream-monaco`,以及 bundler 里的 Monaco worker 配置
- **常见问题**:编辑器空白时,优先检查 worker 打包和 SSR 边界
如果代码块本身就是产品体验的一部分,用它最合适;如果你只是想高亮代码,不需要 Monaco 的编辑能力,优先看 `MarkdownCodeBlockNode`。
深入页面: [CodeBlockNode](/zh/guide/code-block-node)、[Monaco](/zh/guide/monaco)
## MarkdownCodeBlockNode
> 基于 Shiki 和 `stream-markdown` 的轻量代码块渲染器。
- **适合**:SSR 友好的文档站、博客页、更小的打包体积
- **关键 props**:`node`、`stream`、`loading`
- **插槽**:`header-left`、`header-right`
- **同伴依赖**:`stream-markdown`
- **常见问题**:一直没有高亮时,先确认 `stream-markdown` 已安装,并在实际渲染环境里可用
如果你不需要 Monaco 的编辑面板和 diff 交互,这个通常是更轻的选择。
## MermaidBlockNode
> 渐进式 Mermaid 渲染器,带复制、导出、弹窗等交互能力。
- **适合**:大图、AI 生成图表、用户主动导出
- **关键 props**:`node`、`isDark`、`isStrict`、`maxHeight`、`estimatedPreviewHeightPx`
- **事件**:`copy`、`export`、`openModal`、`toggleMode`
- **同伴依赖**:`mermaid` >= 11
- **常见问题**:把 Mermaid 当成客户端增强能力看待;SSR 首包已经有 fallback,真正的 preview 仍然在客户端初始化
`MarkdownRender` 会在调用方未传入时为 Mermaid 围栏估算 `estimatedPreviewHeightPx`。只有在直接使用 `MermaidBlockNode`,或自定义 `mermaid` 渲染器已经知道首屏 preview 高度时,才需要手动传入。
深入页面: [Mermaid](/zh/guide/mermaid)、[MermaidBlockNode](/zh/guide/mermaid-block-node)
## D2BlockNode
> 渐进式 D2 图表渲染器,适合结构化架构图和说明图。
- **适合**:D2 文档、自动生成的架构视图、渐进式渲染
- **关键 props**:`node`、`isDark`、`maxHeight`、`progressiveRender`、`progressiveIntervalMs`
- **同伴依赖**:`@terrastruct/d2`
- **常见问题**:缺少 peer 时会回退显示源码,而不是渲染好的图表
深入页面: [D2 图表](/zh/guide/d2)
## MathBlockNode / MathInlineNode
> 基于 KaTeX 的块级 / 行内公式渲染器。
- **适合**:技术文档、公式说明、AI 数学输出
- **关键 prop**:`node`
- **同伴依赖**:`katex`
- **必需 CSS**:`katex/dist/katex.min.css`
- **常见问题**:公式空白通常是 KaTeX CSS 缺失,而不是解析失败
## ImageNode
> 内置图片渲染器,提供 caption、fallback、lazy 等常用能力。
- **适合**:自定义预览、埋点、lightbox 集成
- **关键 props**:`fallback-src`、`show-caption`、`lazy`、`svg-min-height`、`use-placeholder`
- **事件**:`click`、`load`、`error`
- **常见接法**:先包一层自定义组件,再通过 `setCustomComponents(customId, { image: CustomImageNode })` 注册
深入页面: [ImageNode](/zh/guide/image-node)
## LinkNode
> 带下划线动画和 tooltip 选项的链接渲染器。
- **适合**:文档站、聊天界面、需要和设计系统统一交互样式的链接
- **关键 props**:`color`、`underlineHeight`、`showTooltip`
- **常见问题**:浏览器默认样式或 reset 顺序不对时,看起来会像“组件失效”,先检查 CSS 顺序
## VmrContainerNode
> 用来渲染 `:::` 容器以及解析器归一化后的结构化块。
- **适合**:提示框、Notice、AI 特殊块、容器型自定义组件
- **关键 prop**:`node`,其中包含 `name`、`attrs`、`loading`、`children`
- **归一化细节**:JSON attrs 会整理到 `node.attrs`;无效或不完整 JSON 会保留在 `attrs.attrs`;容器名后面的参数会落到 `attrs.args`
- **常见接法**:和 `custom-html-tags` 或 parser hooks 配合,处理可信的结构化块
## 单独渲染节点组件时的检查清单
如果你绕过 `MarkdownRender`,直接挂载节点组件:
- 外层包上 `.markstream-vue` 容器。
- 引入和完整渲染器一致的 CSS。
- 只安装并初始化当前节点真正需要的 peers。
- 所有替换样式都用父级选择器或 `data-custom-id` 一类的方式做局部作用域。
## 如果你现在找的是别的层级
- 查解析器工具、`setCustomComponents`、AST hooks:看 [API 参考](/zh/guide/api)
- 查完整渲染器配置:看 [Props 与选项](/zh/guide/props)
- 组件选对了但样式 / peers / SSR 还是不对:看 [故障排除](/zh/guide/troubleshooting)
@@ -0,0 +1,26 @@
# 贡献指南
欢迎贡献!
常用开发命令:
- `pnpm dev` / `pnpm play` — 启动 playground
- `pnpm build` — 构建库
- `pnpm build:analyze` — 生成 bundle 可视化报告
- `pnpm size:check` — 执行包体积预算检查
- `pnpm test` — 运行测试
- `pnpm docs:dev` — 启动文档站点
体积预算环境变量(可选):
- `MAX_DIST_BYTES`
- `MAX_JS_CHUNK_BYTES`
- `MAX_PACK_TGZ_BYTES`
- `MAX_PACK_UNPACKED_BYTES`
文档贡献:
1. 新页面写入 `docs/` 或 `docs/zh/`
2. 更新 `docs/.vitepress/config.ts` 的侧边栏
3. 运行 `pnpm docs:dev` 预览
4. 提交 PR 并等待审阅
@@ -0,0 +1,152 @@
---
description: 为 thinking 等可信标签接入自定义组件,在不改解析器的前提下扩展 markstream-vue 的高级渲染能力。
---
# 自定义标签与高级组件
当你的 Markdown 里需要出现 `thinking`、`answer-box` 之类“可信的组件式标签”时,这页就是最直接的入口。
推荐路径是:
1. 用 `custom-html-tags` 声明这个标签
2. 用 `setCustomComponents` 把它映射到自定义组件
3. 用 `custom-id` 把覆盖范围限制在当前业务区域
`custom-html-tags` 里请传标签式名字,比如 `thinking`、`answer-box`、`my_component`。
像 `foo:bar` 这种 namespaced 形式会被忽略;`code_block` 这类内置 override key 会继续保留给节点渲染器覆盖,所以可信自定义标签仍然需要显式写进 `custom-html-tags`。
只有当这条路径不够用时,再去碰解析器钩子。
如果这些标签最终是用在文档站或 VitePress 主题里,建议再配合 [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration) 一起看,把主题层注册和 CSS 顺序一次理顺。
## 1. 最简单的自定义标签接法
```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>
```
一旦这个标签被加入白名单,解析器就会直接产出 `type` 等于标签名本身的自定义节点。
## 2. 一个实用的 Vue 组件写法
自定义标签内部往往还会包含 Markdown。最稳妥的办法,就是在你的自定义组件里再嵌一层 `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>
```
这种“外层自定义组件 + 内层 MarkdownRender”的模式,也是处理重复和嵌套标签时最稳的写法。
## 3. 解析器会给你什么数据
对于可信自定义标签,产出的节点通常会包含:
- `type`:标签名本身,例如 `thinking`
- `tag`:原始标签名
- `content`:标签内部的 Markdown / 文本内容
- `attrs`:提取出来的标签属性
- `loading`:当前是否仍处在流式中间态
- `autoClosed`:流式阶段是否发生过临时自动补闭合
`attrs` 的具体形状可能会因来源而不同,所以更好的做法是把它当成“原始属性容器”,在你的组件里按需归一化。
## 4. 重复和嵌套的自定义标签
这条方案本身就是为这些情况设计的:
- 同一篇文档里多次出现相同自定义标签
- 自定义标签嵌套自定义标签
- 流式输出时,闭合标签还没到达的中间态
实战建议:
- 内层 `MarkdownRender` 继续传同样的 `custom-html-tags`
- 小型嵌套壳子里,如果你想要更稳定的流式行为,可以关闭 batching 和 viewport deferral
- 外层渲染器始终带上 `custom-id`
## 5. 什么情况下 `custom-html-tags` 已经足够
适合直接用 `custom-html-tags` + `setCustomComponents` 的情况:
- 语法本身已经是标签式的
- 内容来源是你信任的
- 你需要的是“换一种渲染方式”,而不是“改一种语法规则”
应该升级到 [高级解析](/zh/guide/advanced) 的情况:
- 标签在进入稳定节点前,还需要先做 token 改写
- 你必须在解析后合并、拆分或重塑节点
- 源格式根本不适合用标签外壳来表达
## 6. 作用域和清理依然重要
即使是自定义标签,也建议默认使用 scoped mapping:
```ts twoslash
import type { Component } from 'vue'
import { removeCustomComponents, setCustomComponents } from 'markstream-vue'
declare const ThinkingNode: Component
setCustomComponents('chat', { thinking: ThinkingNode })
// 之后如果这个作用域不再使用
removeCustomComponents('chat')
```
这样可以把自定义行为牢牢限制在当前页面、路由或业务模块里。
@@ -0,0 +1,110 @@
# D2 快速开始
D2 图表在 markstream-vue 中是渐进式渲染:只要语法可用就会先出预览,后续流式补全时持续更新。如果渲染失败,会保留上一次成功的预览,同时保留源码。
## 1. 安装与引入
```bash
pnpm add @terrastruct/d2
```
无需额外 CSS。保持 `markstream-vue/index.css` 在 reset 之后,并在使用 Tailwind/UnoCSS 时放入 `@layer components`:
```css
@import 'modern-css-reset';
@layer components {
@import 'markstream-vue/index.css';
}
```
## 2. 流式示例
```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>
```
快速试一下:
```md
\`\`\`d2
direction: right
Client -> API: request
API -> DB: query
DB -> API: rows
API -> Client: response
\`\`\`
```
## 3. 高级组件:`D2BlockNode`
如果你需要工具栏(复制/导出/模式切换)或自定义主题、渐进节奏,可直接使用 `D2BlockNode`:
```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:
- `progressiveRender`, `progressiveIntervalMs` 控制流式增量渲染节奏。
- `themeId`, `darkThemeId` 指定 D2 主题。
- `showHeader`, `showModeToggle`, `showCopyButton`, `showExportButton`, `showCollapseButton` 控制工具栏。
## 4. 深色模式
传入 `:is-dark="true"`(或在 `MarkdownRender` 外层添加 `.dark`)即可启用深色模式。组件会以应用主题为准,避免被系统偏好色打断。
## 5. 自定义 loader(可选)
如果你之前手动关闭了 loader,或需要从 CDN/全局变量加载:
```ts twoslash
import { enableD2, setD2Loader } from 'markstream-vue'
// 重新启用默认 loader
enableD2()
// 或设置自定义 loader
setD2Loader(() => (window as any).D2)
```
当 `@terrastruct/d2` 未安装时会回退成源码展示,安装后会自动切换为预览。
@@ -0,0 +1,7 @@
# 文档资源与截图(中文)
如果向文档中添加截图或 GIF,请遵循以下建议:
- 保持文件体积小(优先使用 SVG 或压缩图片)
- 将资源放在 `docs/.vitepress/public/`,以便 VitePress 正确托管
- 使用描述性文件名,并在图片下方添加说明
@@ -0,0 +1,74 @@
# E2E 测试报告(中文)
> 英文版参见:`docs/e2e-testing-report.md`。
本文档汇总了仓库中新增的端到端(E2E)Markdown 解析测试,便于在未来持续扩展覆盖范围。
## 目标
提供一个精简、可复现的 E2E 测试套件,用于验证解析器和渲染器是否能正确处理常见的 Markdown 结构,并作为后续扩展的基础。
## 新增内容
- `test/fixtures/` —— 收录具有代表性的 Markdown 用例:
- `headings.md`
- `code-diff.md`
- `table.md`
- `admonition.md`
- `math.md`
- `footnotes.md`
- `image-link.md`
- `checkbox.md`
- `mermaid.md`
- `test/e2e.markdown.test.ts` —— Vitest 测试文件,主要职责:
- 读取每个 fixture
- 使用 `parseMarkdownToStructure`(基于 `getMarkdown`)进行解析
- 序列化为最小稳定结构并针对每个 fixture 生成快照
- 含有一个针对行内数学/代码块渲染的断言
- 快照 —— 由 Vitest 生成,存放在默认快照目录。
- `.github/workflows/ci.yml` —— GitHub Actions 工作流,在 `push` 与 `pull_request` 时执行:
- 安装依赖
- 运行 `pnpm test`
- 运行 `pnpm typecheck`
- 运行 `pnpm lint`
## 本地运行方式
如需在本地运行测试(并在输出预期发生变化时更新快照):
```bash
pnpm test -- -u
```
仅运行测试(不更新快照):
```bash
pnpm test
```
类型检查与 ESLint:
```bash
pnpm typecheck
pnpm lint
```
## 建议 / 下一步
1. 扩展快照内容(例如加入 `children` 文本片段与节点 `meta`),提升对节点 payload 回归的捕捉能力。
2. 增加以下边界场景的 fixture 与断言:
- 未闭合的代码围栏
- 深度嵌套列表中混合标记
- 转义括号等特殊场景
- 流式代码围栏的异常输入
3. 为需要 DOM 或 Worker 的插件(Mermaid、KaTeX、Monaco)补充集成测试,并考虑在 CI 中加入基于 Playwright 的浏览器流程。
如果需要,我可以:
1. 立即扩充快照的元信息。
2. 补充上述边界场景的 fixture 与测试。
3. 起草一个面向 DOM 插件的 Playwright CI 任务。
欢迎告知下一步的优先项。
@@ -0,0 +1,590 @@
# ECharts 集成
`markstream-vue` 支持通过自定义组件渲染 ECharts 图表。本文介绍三种推荐方案,从最简单到最高级。
## 为什么在 markstream-vue 中使用 ECharts?
ECharts 是一个强大的图表库,与 `markstream-vue` 的流式架构天然契合。你可以:
- 直接在 Markdown 代码块中编写图表配置
- 流式更新图表(适合 AI 生成的内容)
- 复用现有的 ECharts 主题和配置
- 导出图表为图片
---
## 方案一:自定义 CodeBlock 组件(推荐)
此方案将 ECharts 作为一种特殊的代码块语言,类似于 Mermaid 的处理方式。这是对用户最友好的方式,也符合现有架构模式。
### 1.1 创建 ECharts 组件
创建 `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>()
// 只渲染 echarts 代码块
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
// 销毁已存在的实例
if (chartInstance) {
chartInstance.dispose()
}
// 根据暗黑模式初始化主题
const theme = props.isDark ? 'dark' : undefined
chartInstance = echarts.init(chartRef.value, theme)
try {
// 从代码中解析 JSON 配置
const option = JSON.parse(props.node.code)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] 配置无效:', error)
chartInstance?.dispose()
chartInstance = null
}
}
// 主题切换时重新初始化
watch(() => props.isDark, () => {
initChart()
})
// 代码变化时更新图表
watch(() => props.node.code, () => {
if (chartInstance) {
try {
const option = JSON.parse(props.node.code)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] 配置无效:', 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 注册自定义组件
```ts
// main.ts 或组件入口文件
import { setCustomComponents } from 'markstream-vue'
import EChartsBlockNode from './components/EChartsBlockNode.vue'
setCustomComponents({
code_block: EChartsBlockNode
})
```
### 1.3 在 Markdown 中使用
现在可以直接在 Markdown 中编写 ECharts 图表:
```markdown
\```echarts
{
"title": { "text": "销售数据" },
"tooltip": {},
"xAxis": {
"type": "category",
"data": ["周一", "周二", "周三", "周四", "周五"]
},
"yAxis": { "type": "value" },
"series": [{
"type": "bar",
"data": [120, 200, 150, 80, 70]
}]
}
\```
```
### 1.4 安装 ECharts
```bash
pnpm add echarts
# 或
npm install echarts
```
### 优缺点
**优点:**
- 熟悉的代码块语法
- 遵循现有的 Mermaid 模式
- 支持流式更新
- 易于实现
**缺点:**
- 会覆盖所有 `code_block` 的渲染(需要处理非 echarts 代码块)
- 需要 JSON 解析(不支持 JS 表达式)
---
## 方案二:自定义 HTML 标签
使用自定义 HTML 类标签来渲染 ECharts 图表,与常规代码块分离更清晰。
### 2.1 配置自定义 HTML 标签
```ts
// 解析器配置(或在 MarkdownRender 上直接传 `custom-html-tags`)
import { getMarkdown } from 'markstream-vue'
const md = getMarkdown('echarts', {
customHtmlTags: ['echarts', 'chart'],
})
```
### 2.2 注册组件
```ts
import { setCustomComponents } from 'markstream-vue'
import EChartsComponent from './components/EChartsComponent.vue'
setCustomComponents({
echarts: EChartsComponent,
chart: EChartsComponent
})
```
### 2.3 创建组件
```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
// 从属性中解析宽度/高度
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] 配置无效:', 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] 配置无效:', error)
}
}
})
onMounted(initChart)
onBeforeUnmount(() => {
chartInstance?.dispose()
})
</script>
<template>
<div ref="chartRef" class="echarts-chart" :style="{ width, height }" />
</template>
```
### 2.4 在 Markdown 中使用
```markdown
<echarts width="100%" height="500px">
{
"series": [{
"type": "pie",
"data": [
{ "value": 335, "name": "直接访问" },
{ "value": 310, "name": "邮件营销" },
{ "value": 234, "name": "联盟广告" }
]
}]
}
</echarts>
```
### 优缺点
**优点:**
- 类型分离清晰
- 支持自定义属性(宽度、高度)
- 不干扰代码块
**缺点:**
- 需要 parser 配置
- 语法不够熟悉
- 不是标准 Markdown
---
## 方案三:VmrContainer 集成
在自定义容器中使用 ECharts(类似 admonitions),适合带图表说明的文档。
### 3.1 注册容器组件
```ts
import { setCustomComponents } from 'markstream-vue'
import EChartsContainerNode from './components/EChartsContainerNode.vue'
setCustomComponents({
vmr_container: EChartsContainerNode
})
```
### 3.2 创建容器组件
```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>()
// 只处理 echarts 容器
const isEChartsContainer = computed(() => props.node.name === 'echarts')
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
// 从子节点提取 JSON
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 在 Markdown 中使用
```markdown
:::echarts
{
"title": { "text": "流量来源" },
"series": [{
"type": "pie",
"radius": "50%",
"data": [
{ "value": 1048, "name": "搜索引擎" },
{ "value": 735, "name": "直接访问" },
{ "value": 580, "name": "邮件营销" }
]
}]
}
:::
```
### 优缺点
**优点:**
- 与现有容器系统一致
- 支持图表周围添加额外内容
- 适合文档场景
**缺点:**
- 配置较复杂
- 需要解析子节点
---
## SSR 注意事项
ECharts 需要 DOM 环境,不支持服务端渲染。请使用以下策略之一:
### Nuxt 3 中使用
```vue
<template>
<ClientOnly>
<MarkdownRender :content="markdown" />
</ClientOnly>
</template>
```
### SSR 带降级
```vue
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { onMounted, ref } from 'vue'
const isClient = ref(false)
const markdown = '# ECharts 示例'
onMounted(() => {
isClient.value = true
})
</script>
<template>
<MarkdownRender v-if="isClient" :content="markdown" />
<div v-else class="echarts-placeholder">
加载图表中...
</div>
</template>
```
### 组件内检查 window
```ts
import { computed } from 'vue'
const canRender = computed(() => typeof window !== 'undefined' && isECharts.value)
```
---
## 高级功能
### 使用 ResizeObserver 实现响应式图表
```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()
}
}
})
```
### 导出为图片
```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()
}
```
### 使用 ECharts 主题切换
```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)
```
---
## 应该选择哪种方案?
| 方案 | 适用场景 | 复杂度 |
|------|----------|--------|
| **CodeBlock** | 大多数用户,简单集成 | ⭐ |
| **自定义标签** | 高级用户,多种图表类型 | ⭐⭐ |
| **VmrContainer** | 文档场景,带样式容器 | ⭐⭐⭐ |
**推荐:** 从 **方案一(CodeBlock)** 开始。这是最直接的方式,也符合用户与 Mermaid 图表交互的方式。
---
## 完整示例
使用方案一的完整最小设置:
```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'
// 注册自定义组件
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')
```
---
## 故障排除
1. **图表未渲染** — 检查浏览器控制台的 JSON 解析错误。确保代码块内容是有效的 JSON。
2. **容器空白** — 验证 ECharts 已安装且 DOM 元素有尺寸。设置明确的宽高样式。
3. **主题未切换** — 切换主题时需要重新创建 ECharts 实例。使用 `watch(() => props.isDark)` 来销毁并重新初始化。
4. **SSR 错误** — 在 Nuxt 中使用 `<ClientOnly>` 包裹组件,或在初始化前检查 `typeof window !== 'undefined'`。
5. **内存泄漏** — 始终在 `onBeforeUnmount` 中调用 `chartInstance.dispose()`。
仍有问题?请查阅 [ECharts 官方文档](https://echarts.apache.org/zh/index.html)或在 GitHub 上提 issue。
@@ -0,0 +1,45 @@
# 示例
简明示例;交互演示请查看 `playground`。以下为中文示例并附带注释:
## 流式 Markdown
这是将字符逐步添加到 `content` 中并让 `MarkdownRender` 渲染的最小示例:
```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>
```
## 使用 `nodes`
如果你想跳过字符串解析阶段,可直接传入解析后的节点:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# Hello', md)
// 在 Vue 中传入 nodes 而不是 content
```
更多示例请查看 `/zh/guide/examples` 或 demo playground 页面。
@@ -0,0 +1,12 @@
# 特性
- ⚡ **超高性能**:针对流式场景优化,最小化重渲染和 DOM 更新
- 🌊 **流式优先设计**:原生支持增量/不完整的 Token 化 Markdown
- 🧠 **Monaco 流式更新**:为大代码块提供平滑的增量编辑体验
- 🪄 **渐进式 Mermaid**:图表在语法可用时尽快渲染并逐步完善
- 🪄 **渐进式 D2**:语法可用即预览,失败时保留上次成功渲染
- 🧩 **自定义组件**:允许在 Markdown 内容中嵌入 Vue 组件
-- 📝 **完整 Markdown 支持(可选 Emoji 插件)**:表格、公式、复选框、代码块等。Emoji 支持现作为插件提供,默认不启用,需要在 `getMarkdown` 配置或通过组件的 `customMarkdownIt` prop 显式注册 `markdown-it-emoji`。
- 🔌 **零配置出箱**:适用于 Vue 3 项目
要了解更多示例,请查看文档页:`/zh/guide/usage`、`/zh/guide/features`。
@@ -0,0 +1,53 @@
# ImageNode — 自定义图片预览
`ImageNode` 在内部渲染图片并在用户交互时发出事件。我们为图片点击暴露了一个事件,因此你可以接手点击行为(例如打开自定义图片预览/Lightbox),而无需替换整个渲染器。
- 事件:`click` — 当用户在成功加载的图片上触发点击时发出。负载形如 `[Event, string]`,其中第二项为最终用于渲染的图片 URL(可能是 fallback)。
- 事件:`load` / `error` — 图片加载成功或失败事件,负载为图片 `src`。
示例:创建一个包装组件来拦截 `click` 事件,并在 VitePress 中注册它作为自定义组件。
1) `CustomImageNode.vue`(包装原始 `ImageNode`,拦截点击并在新标签打开图片作为简易预览):
```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
// 简单示例:在新标签页打开图片用于预览
// 在真实项目中可替换为 modal/lightbox 组件
window.open(src, '_blank')
// 仍然向外暴露 click 事件,供上层处理
emit('click', payload)
}
</script>
<template>
<ImageNode v-bind="$attrs" @load="emit('load', $event)" @error="emit('error', $event)" @click="onImageClick" />
</template>
```
2) 在 VitePress 主题增强(`docs/.vitepress/theme/index.ts` 的 `enhanceApp`)中注册自定义组件:
```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 接受一个 id(任意字符串,用于区分不同注册)和组件映射
// 通过这种方式,库在渲染 ImageNode 时会使用你提供的组件
setCustomComponents('vitepress-image-preview', { image: CustomImageNode })
}
```
可选建议:
- 若希望更丰富的预览体验(缩放、手势、幻灯片),可在 `onImageClick` 中调用诸如 `photoswipe`、`fslightbox` 等库,或使用应用级 modal 管理预览状态。
说明小结:
- `ImageNode` 提供的 `click` 事件是接管自定义预览的入口;常见做法是用一个包装组件替换默认实现并通过 `setCustomComponents` 注册到 VitePress/客户端应用中。
- 包装组件也可以复用 `load` / `error` 事件,或在内部实现更多逻辑(统计、懒加载补偿、占位符策略等)。
@@ -0,0 +1,76 @@
---
description: 按任务组织的 markstream-vue 指南,帮助你在安装、流式渲染、VitePress 文档站、组件覆盖和迁移之间快速找到正确入口。
---
# 指南
这份文档优先按“用户任务”组织,而不是默认要求你从第一页看到最后一页。先从和你当前问题最接近的入口开始。
## 我想先把它跑起来
- [安装](/zh/guide/installation):先按场景装对 peer 依赖,并理顺 CSS 顺序。
- [快速开始](/zh/guide/quick-start):最小 Vue 3 示例。
- [使用与流式渲染](/zh/guide/usage):决定该用 `content` 还是 `nodes`。
- [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration):把文档页、`enhanceApp`、可信标签和 CSS 顺序串起来。
- [AI 聊天与流式输出](/zh/guide/ai-chat-streaming):当页面持续更新时,一次看完 peers、解析、性能和排障。
- [按症状排查](/zh/guide/troubleshooting-path):先判断问题属于样式、peers、SSR,还是自定义标签接法。
- [Props 与选项](/zh/guide/props):已经能跑,只是需要调配置。
## 我想做业务定制
- [API 参考](/zh/guide/api):解析器工具、作用域覆盖和渲染流程入口。
- [渲染器与节点组件](/zh/guide/components):导出的渲染器和节点组件参考。
- [覆盖内置组件](/zh/guide/component-overrides):替换 `image`、`code_block`、`mermaid`、`link` 等内置节点。
- [自定义标签与高级组件](/zh/guide/custom-components):支持 `thinking` 这类可信标签。
- [高级解析](/zh/guide/advanced) 与 [解析器 API](/zh/guide/parser-api):需要 token / AST 级改造时再看。
## 我想接入到现有项目里
- [Nuxt SSR](/zh/nuxt-ssr):处理浏览器专属依赖和 `ClientOnly`。
- [AI / Skills 工作流](/zh/guide/ai-workflows):复制即用的任务模板、skill 清单和迁移提示词。
- [从 react-markdown 迁移](/zh/guide/react-markdown-migration) 与 [迁移 Cookbook](/zh/guide/react-markdown-migration-cookbook):适合 React 团队。
- [故障排除](/zh/guide/troubleshooting):安装能跑但样式、SSR 或 peers 出问题时先看。
## 框架入口
### Vue 3 (markstream-vue) ⭐ 推荐
| 页面 | 描述 |
|------|------|
| [安装](/zh/guide/installation) | 只安装你真正需要的依赖组合 |
| [快速开始](/zh/guide/quick-start) | 渲染第一篇 Markdown |
| [使用与流式渲染](/zh/guide/usage) | 决定 `content` 与 `nodes` 的接入方式 |
| [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration) | 文档页、`enhanceApp`、可信标签与 CSS 顺序的一站式路径 |
| [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) | 适合聊天 UI、SSE 与逐 token 输出的场景路径 |
| [API 参考](/zh/guide/api) | 解析器工具、作用域覆盖和渲染流程入口 |
| [渲染器与节点组件](/zh/guide/components) | 渲染器和节点组件总览 |
| [自定义](/zh/guide/component-overrides) | 覆盖内置组件与扩展自定义标签 |
### Vue 2 (markstream-vue2)
| 页面 | 描述 |
|------|------|
| [安装](/zh/guide/vue2-installation) | Vue 2 专用安装 |
| [快速开始](/zh/guide/vue2-quick-start) | Vue 2 示例 |
| [组件与 API](/zh/guide/vue2-components) | Vue 2 组件参考 |
### React (markstream-react)
| 页面 | 描述 |
|------|------|
| [安装](/zh/guide/react-installation) | React 专用安装 |
| [快速开始](/zh/guide/react-quick-start) | React 示例 |
| [React 组件](/zh/guide/react-components) | React 渲染器和节点组件 |
| [从 react-markdown 迁移](/zh/guide/react-markdown-migration) | 面向现有 React Markdown 项目的迁移路径 |
| [迁移 Cookbook](/zh/guide/react-markdown-migration-cookbook) | 常见迁移场景的 before/after 示例 |
### Angular (markstream-angular)
| 页面 | 描述 |
|------|------|
| [安装](/zh/guide/angular-installation) | Angular 专用安装 |
| [快速开始](/zh/guide/angular-quick-start) | standalone Angular 示例 |
### Nuxt
- [Nuxt SSR 指南](/zh/nuxt-ssr):处理客户端边界、workers 和浏览器专属 peers。
@@ -0,0 +1,35 @@
# AntV Infographic
`markstream-vue` 支持渲染 [AntV Infographic](https://infographic.antv.vision/) 图表。
## 1. 安装
该功能依赖 `@antv/infographic` 库。
```bash
npm install @antv/infographic
```
## 2. 示例
在 Markdown 中使用 `infographic` 代码块即可渲染图表:
````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
```
````
![Infographic demo](/screenshots/infographic-demo.png)
## 3. 更多资源
- [AntV Infographic 官网](https://infographic.antv.vision/) - 查看更多图表模版与语法的详细介绍。
@@ -0,0 +1,126 @@
---
description: 按文档站、AI 聊天、Mermaid、KaTeX、Monaco 和大文档场景选择正确的 markstream-vue 安装与 peer 依赖组合。
---
# 安装
如果你只是想先把渲染器跑起来,先装主包,再按能力补装 peer 依赖就够了。
## 1. 最小安装
```bash
pnpm add markstream-vue
# 或
npm install markstream-vue
# 或
yarn add markstream-vue
```
只需要普通 Markdown 渲染时,装完主包就可以继续看 [快速开始](/zh/guide/quick-start)。
## 2. 按能力选择 peer 依赖
| 能力 | 需要的包 | 适用场景 |
|------|---------|---------|
| 轻量高亮代码块 | `stream-markdown` | 文档站、SSR、包体积敏感场景 |
| Monaco 代码块 | `stream-monaco` | 需要复制、预览、展开、字体控制等完整代码块体验 |
| Mermaid 图表 | `mermaid` | 渲染 `mermaid` fenced code block |
| D2 图表 | `@terrastruct/d2` | 渲染 `d2` / `d2lang` fenced code block |
| KaTeX 数学公式 | `katex` | 行内或块级公式 |
## 3. 常见安装配方
### 文档站 / SSR 优先
```bash
pnpm add markstream-vue stream-markdown
```
如果你接下来要做的是文档站、内容站或 VitePress 主题,继续看 [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration)。
### AI / 聊天 UI,且需要更强的代码块和图表体验
```bash
pnpm add markstream-vue stream-monaco mermaid katex
```
然后继续看 [AI 聊天与流式输出](/zh/guide/ai-chat-streaming),里面会把 `nodes` + `final` 的推荐接法和聊天场景调优串起来。
### 图表型内容较多
```bash
pnpm add markstream-vue mermaid @terrastruct/d2 katex
```
### 一次性全开
```bash
pnpm add markstream-vue stream-markdown stream-monaco mermaid @terrastruct/d2 katex
```
## 4. CSS 顺序和安装同样重要
主包入口已经带了默认样式,但如果你的项目有 reset、Tailwind 或 UnoCSS,通常还是建议显式控制 CSS 顺序:
```css
@import 'modern-css-reset';
@tailwind base;
@layer components {
@import 'markstream-vue/index.css';
}
```
如果你使用数学公式,还要额外引入 KaTeX 的 CSS:
```ts
import 'katex/dist/katex.min.css'
```
`stream-monaco`、`mermaid` 和 `@terrastruct/d2` 不需要再从本包额外引入 CSS。
## 5. 功能加载器(只在 CDN 或自定义控制时需要)
装好 peers 以后,默认 loader 已经启用。只有当你手动关闭过,或者想切到 CDN / 自定义 loader 时,才需要显式调用:
```ts twoslash
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue'
enableMermaid()
enableKatex()
enableD2()
```
## 6. 第一次接入后的检查清单
- 如果你单独使用节点组件,而不是 `MarkdownRender`,请外层包一层 `<div class="markstream-vue">...</div>`。
- 如果公式不显示,优先检查 `katex` 是否安装、CSS 是否导入。
- 如果 Monaco 空白,优先检查 worker 打包和浏览器端执行时机。
- 如果样式错乱,先看 [故障排除](/zh/guide/troubleshooting#css-looks-wrong-start-here)。
## 7. 快速测试
```vue twoslash
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
const md: MarkdownRenderProps['content'] = '# 你好,markstream-vue!'
const customId: MarkdownRenderProps['customId'] = 'install-check'
</script>
<template>
<MarkdownRender
:content="md"
:custom-id="customId"
/>
</template>
```
下一步建议:
- [快速开始](/zh/guide/quick-start):最小接入
- [使用与流式渲染](/zh/guide/usage):`content` 和 `nodes` 的选择
- [AI 聊天与流式输出](/zh/guide/ai-chat-streaming):聊天 UI、SSE 与逐 token 输出的完整路径
- [覆盖内置组件](/zh/guide/component-overrides):需要业务定制时再进入
@@ -0,0 +1,24 @@
// Copied KaTeX zh page to the guide subfolder for proper zh routing
# KaTeX Worker 性能分析指南(中文)
## 问题:Worker 是否比直接渲染更合适?
本文档帮助你判断在何种场景下使用 Web Worker 会带来性能优势。
## 快速结论
**是的,Worker + Cache 架构在大多数真实场景中有显著优势。**
关键原因:
1. Cache 能消除大部分重复渲染开销(缓存命中率通常 >70%)
2. Worker 能阻止主线程被阻塞,从而保证 UI 响应
3. 内存占用极小(举例:200 个公式大约 10–50KB)
## 简要对比(场景)
- 场景:单个简单公式
- 直接渲染:约 2–5ms
- Worker:约 3–7ms(含通信开销)
- 结论:Worker 略慢,但差异可以忽略
... (rest of content copied from top-level zh KaTeX) ...
@@ -0,0 +1,60 @@
# Legacy 构建与兼容性指南(Vite / Webpack)
目的
- 本文档说明如何使用 legacy 构建以增强旧浏览器(例如较低版本 iOS / Safari)的兼容性,以及何时应该在源码层修复问题。
重要提醒
- 部分问题(尤其是正则引擎级语法,如 lookbehind `(?<!...)`)无法通过 polyfill 修复。legacy 构建可以转换语法和注入 API polyfill,但不能向旧的 JS 运行时“增加”正则引擎特性。如果遇到这类问题,必须在源码层重写或延迟构造并运行时检测/回退。详见 `ios-regex-compat` 文档。
Vite(推荐)
安装:
```bash
pnpm add -D @vitejs/plugin-legacy
```
示例配置(`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,
})
]
})
```
注意:
- `targets` 决定哪些语法/Polyfill 需要应用。根据需要支持的最低版本设置。
- `additionalLegacyPolyfills` 可以把运行时库注入到 legacy bundle。
- `renderLegacyChunks` 生成按需加载的 legacy chunk,并配合 `type="module"` / `nomodule` 差异化加载。
- 但 plugin-legacy 无法改写正则 lookbehind 等引擎级语法;此类问题仍需在源码层处理。
Webpack
常见做法是用 Babel 做两套构建(modern + legacy),通过 `type="module"` / `nomodule` 差异化服务。示例依赖与 `babel.config.js` 请参考英文文档上半部分。
Polyfills
- 使用 `core-js`(v3)和 Babel 的 `useBuiltIns: 'usage'`,并仅在 legacy 构建中注入需要的 polyfill。
正则语法特殊说明
- 引擎级正则语法必须在源码层处理:改写或运行时延迟构造并做 feature-detect。
测试与 CI
- 在 CI 中运行 modern + legacy 构建,并在旧设备/模拟器上做 smoke tests。可加入静态检查阻止新提交引入高级正则写法。
快速检查(本地)
```bash
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
```
总结
- 优先在源码层修复不兼容的正则语法;使用 legacy 构建来覆盖语法与 API 差异,并在 CI/QA 中验证旧设备表现。
@@ -0,0 +1,42 @@
# 数学公式(KaTeX)
`markstream-vue` 在检测到 `katex` 时使用 KaTeX 渲染数学。KaTeX 为可选 peer 依赖,需单独安装。
安装示例:
```bash
pnpm add katex
```
在入口文件导入样式:
```ts
import 'katex/dist/katex.min.css'
```
## CDN 用法(无 bundler)
如果你通过 CDN `<script>` 引入 KaTeX(即 `window.katex` 存在),库会优先使用该全局对象。若 KaTeX 是在首次渲染之后才加载完成,请重置一次 loader:
```ts
import { createKaTeXWorkerFromCDN, enableKatex, setKatexLoader, setKaTeXWorker } from 'markstream-vue'
// 主线程使用 CDN 全局(UMD)
setKatexLoader(() => (window as any).katex)
enableKatex(() => (window as any).katex)
// 可选:把 renderToString 放到 worker(worker 内部同样从 CDN 加载 KaTeX)
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)
```
若没有安装 KaTeX,数学表达式将被保留为纯文本以保证 SSR 安全。
注意:在撰写源码 Markdown 时,请务必在 TeX 括号定界符前使用字面(转义)反斜杠。
也就是说应写成 `\\(...\\)` 而不是 `\(...\)`,以便解析器能够可靠地识别行内 TeX。
未转义的 `\(...\)` 无法与普通括号区分,可能不会被解析为数学公式。
@@ -0,0 +1,46 @@
# 在 MarkdownRender 中覆盖 MermaidBlockNode(示例)
如果你想在 `MarkdownRender` 中完全替换 `mermaid` 渲染器(例如在 `playground-demo`),可以使用 `setCustomComponents`,为 `mermaid` 提供一个 programmatic 的渲染函数,并传入自定义事件处理器来拦截组件的默认行为。
示例:
```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,
// 使用 `h()` 时,通过驼峰的 on<EventName> 传入事件监听器
onExport: (ev: MermaidBlockEvent) => {
// 组件在事件中同时暴露 `svgElement` 和 `svgString`,直接优先使用 ev.svgString
const svgEl = ev.svgElement as SVGElement | null
const svgStringFromEv = ev.svgString as string | null
const svgString = svgStringFromEv ?? (svgEl ? new XMLSerializer().serializeToString(svgEl) : null)
if (svgString) {
// 将 svgString 上传或保存到后端
uploadSvgToServer(svgString)
}
// 阻止组件默认的导出行为
ev.preventDefault()
},
onCopy: (ev: MermaidBlockEvent<{ type: 'copy', text: string }>) => {
ev.preventDefault()
// 复制文本在 ev.payload.text 中(你也可以在这里做埋点/自定义 toast)
const text = ev.payload?.text ?? ''
console.log('MermaidBlockNode copy text:', text)
},
}),
})
```
要点:
- 在使用 `h()` programmatic 渲染组件时,事件监听器应使用 `on<EventName>` 的驼峰形式(例如 `onExport`、`onCopy`)。
- 在监听器中调用 `ev.preventDefault()` 可以阻止 `MermaidBlockNode` 的默认处理,完全由你接管行为(如上传、存储或自定义下载)。
- `MermaidBlockEvent` 可能会在 `ev.svgElement` 中提供渲染后的 `svg` DOM 节点,便于你直接序列化或修改它。
如果你希望我把这个示例内联到组件 README 中或把该页面加入 docs 侧栏,我可以继续帮你完成。
@@ -0,0 +1,79 @@
# MermaidBlockNode (Component)
`MermaidBlockNode` 提供一个易于使用且可扩展的 Mermaid 渲染器,适用于需要在文档中内嵌交互式 Mermaid 图的场景。组件支持渐进式渲染、源码/预览切换、复制源码、导出 SVG,以及伪全屏查看(带缩放与拖拽)。
## Props(关键)
- `node: any` — Mermaid 代码节点(必须)
- `isDark?: boolean` — 暗色模式开关
- `loading?: boolean` — 初始加载占位
- `maxHeight?: string | null` — 最大高度
- `estimatedPreviewHeightPx?: number` — Mermaid 完成渲染前预留的首屏 preview 高度;通过 `MarkdownRender` 渲染 Mermaid 围栏时会自动填入
- `isStrict?: boolean` — 开启 `securityLevel: 'strict'` + DOMPurify,并禁用 HTML labels;用于渲染不可信的 Mermaid 输入(例如用户/LLM 生成的内容),防止 `<script>`、`javascript:` 链接或内联事件落入最终 SVG
- Header/按钮控制(全部可选,默认 `true`):
- `showHeader` / `showModeToggle` / `showCopyButton` / `showExportButton` / `showFullscreenButton` / `showCollapseButton` / `showZoomControls` / `showTooltips`
- `enableWheelZoom?: boolean` — 开启 Ctrl/Cmd + 滚轮缩放(默认 `false`)
- 超时(毫秒):`workerTimeoutMs` / `parseTimeoutMs` / `renderTimeoutMs` / `fullRenderTimeoutMs`(默认 `1400/1800/2500/4000`)
- 流式调优:`renderDebounceMs` / `contentStableDelayMs` / `previewPollDelayMs` / `previewPollMaxDelayMs` / `previewPollMaxAttempts`
## Slots
- `header-left` — 替换左侧(默认是 Mermaid 图标 + 标签)
- `header-center` — 替换中间区域(默认 preview/source 切换)
- `header-right` — 替换右侧操作按钮(完整接管默认按钮)
## Emits
组件发出的事件都使用统一的 `MermaidBlockEvent` 对象,支持 `preventDefault()` 来阻止组件默认行为:
- `copy` — 点击复制按钮,处理签名:`(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)`(调用 `ev.preventDefault()` 可阻止组件默认的复制到剪贴板与 “Copied” 状态)
- `export` — 导出按钮点击,处理签名:`(ev: MermaidBlockEvent<{ type: 'export' }>)`
- `openModal` — 请求打开 pseudo-fullscreen,处理签名:`(ev: MermaidBlockEvent<{ type: 'open-modal' }>)`(组件内部 emit 名称为 `openModal`)
- `toggleMode` — 切换 `source | preview`,处理签名:`(target: 'source' | 'preview', ev: MermaidBlockEvent<{ type: 'toggle-mode'; target: 'source' | 'preview' }>)`(组件内部 emit 名称为 `toggleMode`)
### 拦截示例
完全替换组件默认导出行为:
```vue
<script setup lang="ts">
import type { MermaidBlockEvent } from 'markstream-vue'
import { MermaidBlockNode } from 'markstream-vue'
function onExport(ev: any /* MermaidBlockEvent */) {
ev.preventDefault()
// 组件在事件对象中暴露了 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>
```
> 注意:`export` / `openModal` 事件的事件对象现已同时包含 `svgElement`(DOM 节点)和 `svgString`(已序列化的 SVG 字符串),任选其一使用更方便。
## Slot 示例:完全接管右侧操作按钮
```vue
<MermaidBlockNode :node="node" :showExportButton="false">
<template #header-right>
<button @click="downloadSvg">Download</button>
<button @click="openCustomModal">Open custom modal</button>
</template>
</MermaidBlockNode>
```
## 推荐用法
- 如果你要实现自定义导出/上传,最佳做法是:在 `export` 监听器中 `preventDefault()`,并在监听回调中直接从组件渲染的 DOM 中读取 `svg`。
- 如果你想要完全替换头部的 UI,使用 `header-*` 插槽并把相应 `show*` props 设为 `false` 来隐藏默认按钮。
- 若 Mermaid 内容来自用户/LLM 或任何不可信来源,建议加上 `:is-strict="true"`,组件会对 SVG 进行清理并禁用 HTML labels,从而堵住恶意 `<script>` 或 `javascript:` 链接注入的风险。
## 参考
- 覆盖 `MermaidBlockNode`(在 `MarkdownRender` 中使用 `setCustomComponents`)的示例(中文):[覆盖 MermaidBlockNode(MarkdownRender 示例)](./mermaid-block-node-override.md)
Playground 示例:仓库包含一个可运行的 playground 页面演示如何拦截 `export` 并上传 `ev.svgString` —— 路由: `mermaid-export-demo` (文件:`playground/src/pages/mermaid-export-demo.vue`)。
@@ -0,0 +1,124 @@
# Mermaid 导出示例
仓库包含一个可运行的 playground 示例,展示如何拦截 `MermaidBlockNode` 的 `export` 事件并上传序列化的 SVG(`ev.svgString`)。
const md = `
\`\`\`mermaid
graph LR
A[User] --> B[Server]
B --> C[Storage]
\`\`\`
`
- Playground 路由: `/mermaid-export-demo`
- 文件: `playground/src/pages/mermaid-export-demo.vue`
为什么要看这个示例?
- 演示如何使用 `setCustomComponents` 在 `MarkdownRender` 中替换 mermaid 渲染器。
- 展示如何拦截 `export` 并使用 `ev.svgString`(或 `ev.svgElement`)上传或发送 SVG 到 API。
试试:运行 playground 并访问路由 `/mermaid-export-demo`。
可复制运行示例
以下是一个可直接复制到项目或 playground 的单文件组件示例(SFC)。它展示了如何通过 `setCustomComponents` 覆盖 mermaid 渲染器、拦截 `export` 事件并上传 `ev.svgString`。
```vue
<script setup lang="ts">
import MarkdownRender, { MermaidBlockNode, setCustomComponents } from 'markstream-vue'
import { h } from 'vue'
async function uploadSvgToServer(svgString: string) {
// 这里替换为真实上传逻辑
// 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,
onExport: async (ev: any) => {
ev.preventDefault()
const svg = ev.svgString ?? (ev.svgElement ? new XMLSerializer().serializeToString(ev.svgElement) : null)
if (!svg) {
console.warn('未能获取 svg 数据')
return
}
const url = await uploadSvgToServer(svg)
console.log('svg 已上传到', url)
},
})
})
const md = `
\`\`\`mermaid
graph LR
A[User] --> B[Server]
B --> C[Storage]
\`\`\`
`
</script>
<template>
<MarkdownRender custom-id="my-demo" :content="md" />
</template>
```
在仓库 playground 中运行:
```bash
pnpm play
# 打开 http://localhost:5173/mermaid-export-demo
```
实时演示(内嵌)
如果官方 playground 有托管,下面的 iframe 会直接加载示例(也可以在本地运行 playground 并打开该路由):
<div style="max-width:960px; margin: 1rem 0; position:relative;">
<iframe
src="https://markstream-vue.simonhe.me/mermaid-export-demo"
title="Mermaid 导出示例"
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>
<div style="margin-top:0.5rem; font-size:0.9rem; color:var(--vp-c-default);">
如果内嵌框(iframe)显示为空白或黑色,远端托管站点可能不允许被嵌入(X-Frame-Options/CSP)或示例站点离线。
<strong>你可以尝试:</strong>
<ul>
<li><a href="https://markstream-vue.simonhe.me/mermaid-export-demo" target="_blank" rel="noopener noreferrer">在新标签页打开演示</a></li>
<li>在本地运行 playground:<code>pnpm play</code>,然后访问 <code>/mermaid-export-demo</code></li>
<li>使用下面的 CodeSandbox 链接把 playground 导入到在线编辑器并运行</li>
</ul>
<!-- 视觉预览:使用仓库 docs 的公共截图资源 -->
<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 导出示例预览" style="width:100%; display:block; background:#fff" />
<div style="padding:0.5rem 0.75rem; font-size:0.85rem; color:var(--vp-c-muted);">预览:Mermaid 导出示例(如果 iframe 阻止嵌入,请在本地运行查看)</div>
</div>
</div>
</div>
在 CodeSandbox 中打开
如果希望在 web 编辑器中修改并测试,可以导入仓库到 CodeSandbox(GitHub 导入)并打开 `playground` 文件夹:
CodeSandbox(在线可编辑)
<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);">
在 CodeSandbox 中编辑并运行 playground — 如果需要完整编辑器体验,也可以在新标签页打开:
<a href="https://codesandbox.io/s/github/Simon-He95/markstream-vue/tree/main/playground" target="_blank" rel="noopener noreferrer">在 CodeSandbox 中打开 playground</a>
</div>
</div>
[在 CodeSandbox 中打开 playground(GitHub 导入)](https://codesandbox.io/s/github/Simon-He95/markstream-vue/tree/main/playground)
@@ -0,0 +1,97 @@
# Mermaid 快速上手
`markstream-vue` 支持渐进式 Mermaid 渲染:一旦语法合法就立即生成图表,后续 token 会继续完善图。以下内容介绍安装、流式示例以及常见排障。
## 1. 安装与样式导入
```bash
pnpm add mermaid
```
Mermaid 不需要额外的 CSS 文件。使用 Tailwind/UnoCSS 时,请在 reset 之后、`@layer components` 中导入库的 CSS,避免 utilities 覆盖:
```css
@import 'modern-css-reset';
@layer components {
@import 'markstream-vue/index.css';
}
```
## 2. 流式示例
```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 将在内容流入时逐步出现 -->
</template>
```
快速测试 — 将以下 Markdown 粘贴到任意组件中:
```md
\`\`\`mermaid
graph LR
A[Start]-->B
B-->C[End]
\`\`\`
```
![Mermaid demo](/screenshots/mermaid-demo.svg)
## 3. 进阶组件:`MermaidBlockNode`
若需要头部控制、导出按钮、伪全屏等能力,请参考 [`MermaidBlockNode`](/zh/guide/mermaid-block-node) 或通过 [setCustomComponents 进行覆盖](/zh/guide/mermaid-block-node-override)。仓库内的 playground 提供 `/mermaid-export-demo` 路由可直接试用。
如果渲染的 Mermaid 来自用户/LLM 等不可信来源,可加上 `:is-strict="true"` 启用严格模式与 SVG 清理,阻断 HTML labels 或内联事件混入渲染结果。
## 4. 常见问题排查
1. **未安装依赖**:确保执行 `pnpm add mermaid`。缺失时组件会退回显示原始文本。
2. **版本过旧**:请使用 `mermaid` ≥ 11,旧版本无法兼容异步渲染。
3. **SSR 报错**:Mermaid 依赖 DOM。Nuxt 请包裹 `<ClientOnly>`,Vite SSR 请在 `onMounted` 中渲染。
4. **图表过大**:考虑在服务端预渲染或缓存 SVG,`MermaidBlockNode` 导出事件中提供 `svgString` 可直接上传或持久化。
若问题仍存在,请在 playground (`pnpm play`) 中构造最小示例并附带链接提交 issue,方便复现与定位。
## CDN 用法(无 bundler)
如果你通过 CDN 引入 Mermaid,并希望流式场景下的解析(语法检查/前缀查找)在 worker 中进行,可以注入一个“CDN 加载 Mermaid”的 worker:
```ts twoslash
import { createMermaidWorkerFromCDN, enableMermaid, setMermaidLoader, setMermaidWorker } from 'markstream-vue'
// 主线程使用 CDN 全局(UMD)
setMermaidLoader(() => (window as any).mermaid)
enableMermaid(() => (window as any).mermaid)
// 可选:worker 用于流式解析/前缀查找(推荐 module worker)
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,42 @@
# Monaco 内部实现
本文档介绍在 `markstream-vue` 中使用 `monaco-editor` 时的打包和运行时要点(用于在项目中集成大型编辑器时减少问题)。
## 安装
```bash
pnpm add stream-monaco
```
## Vite 打包建议
- Monaco 使用 Web Worker 来处理语法高亮与语言服务;在生产环境下需要正确的 worker 配置。
- 推荐使用 `vite-plugin-monaco-editor-esm` 或 `vite-plugin-monaco-editor` 来确保 worker 文件被正确打包。
示例:
```ts
import { defineConfig } from 'vite'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({ plugins: [monacoEditorPlugin()] })
```
## 避免 `Failed to load Monaco worker` 错误
- 确认插件将 worker 文件打包到可访问的路径(可通过插件提供的 `customDistPath` 配置来控制)。
- 在构建产物中检查 worker 文件是否存在,并确认 CDN 或静态资源路径可访问。
如果你将 `markstream-vue` 发布为库,请确保使用者把 `stream-monaco` 作为 peer 依赖安装;否则在运行时找不到 Monaco worker 与 runtime。
## 运行时行为与性能
- 本库设计为当 `CodeBlockNode` 挂载时懒加载 Monaco,从而减少首次渲染时的包体积。仅在需要时才初始化 Monaco 编辑器。
- 在大量 CodeBlock 的页面中,启用 `viewportPriority` 或在父组件中延迟加载可以显著降低内存占用并提升首屏速度。
- 如果宿主应用希望更早预热 Monaco,请调用 `markstream-vue` 暴露的 `preloadCodeBlockRuntime()`,不要直接 import `stream-monaco` 做预热。这样 Monaco worker 预热和 markstream-vue 内部的代码块 runtime-ready 状态会保持一致。
## 打包到库的注意事项
- 当把本库打包进其他项目时,请在目标项目中正确配置 worker 路径或使用插件的选项来重新定位。
- 可在页面需要时调用 `preloadCodeBlockRuntime()` 预加载代码块运行时,以改善首次打开大型文档时的响应,并避免已预热后的代码块重新挂载时闪回 `<pre>` fallback。
- 已有代码如果使用 `getUseMonaco()`,可以保持不变;它也会设置相同的 runtime-ready 状态。
## 常见故障排查
- 如果看到 `Failed to load Monaco worker`,先在浏览器网络面板(Network)中检查 worker 文件的请求与响应;资源 404 或路径不正确通常就是问题所在。
- 如果在 SSR 场景报错,请确认 Monaco 仅在客户端初始化(使用动态导入或 `onMounted` 来隔离)。
@@ -0,0 +1,194 @@
# Monaco 编辑器集成
Monaco 编辑器集成为可选功能(由 `stream-monaco` 提供)。它支持对大代码块进行快速的增量更新。
安装:
```bash
pnpm add stream-monaco
```
提示:
- 延迟初始化 Monaco(仅在可见或需要时才初始化)
- 在生产构建中需要配置 worker 打包(使用 `vite-plugin-monaco-editor-esm`)
- 如果需要更快的首次渲染,可使用 `preloadCodeBlockRuntime()` 在应用启动、空闲时间或路由进入前预加载代码块运行时
- 不需要额外导入 CSS
更多细节参见 `/zh/guide/monaco-internals`。
## Vue CLI(Webpack 4)限制
`stream-monaco` 内部会使用 `import.meta.url` 来定位 Monaco worker 资源,这在 **Webpack 4** 中无法正确编译/解析。对于 Vue CLI 4(Webpack 4)项目:
- 建议升级到 Webpack 5 或迁移到 Vite,以获得最稳定的 Monaco 体验。
- 如果必须停留在 Webpack 4:更推荐把 `code_block` 切到基于 Shiki 的渲染(`stream-markdown`),通过覆盖 `code_block` 来实现(见 [/zh/guide/code-blocks](/zh/guide/code-blocks))。也可以在 `vue.config.js` 中显式忽略 Monaco 相关依赖:
```js
// vue.config.js
const webpack = require('webpack')
module.exports = {
configureWebpack: {
plugins: [
new webpack.IgnorePlugin({ resourceRegExp: /^stream-monaco$/ }),
new webpack.IgnorePlugin({ resourceRegExp: /^monaco-editor$/ }),
],
},
}
```
### Webpack & MonacoWebpackPlugin
如果你的项目使用 `monaco-editor-webpack-plugin` 来打包 Monaco,请让该插件通过 `globalThis.MonacoEnvironment` 接管 worker 的解析与路径。`markstream-vue` 在检测到 `MonacoEnvironment.getWorker/getWorkerUrl` 已存在时,不会再覆盖它们。
### 预加载 Monaco
为了避免首次代码块挂载时出现 cold-start fallback 闪烁,可以在应用初始化、空闲时间或进入含代码块的路由前预加载:
```ts
import { preloadCodeBlockRuntime } from 'markstream-vue'
void preloadCodeBlockRuntime()
```
`preloadCodeBlockRuntime()` 会通过 markstream-vue 内部路径动态加载 `stream-monaco`、注册 Monaco workers,并标记代码块运行时已就绪。这样后续 `CodeBlockNode` 重新挂载时可以跳过 `<pre>` 加载 fallback。
已有代码如果调用的是 `getUseMonaco()`,可以保持不变;它现在也会设置相同的 runtime-ready 状态,并继续返回已加载的 `stream-monaco` module 或 `null`。
### 添加更多语言与主题
为了保持初始化速度,默认只注册了一小部分 Monaco 语言。如果你的文档需要 Rust、Go、Bash 等额外语法,或希望注入自定义主题,可以将它们通过 `monacoOptions` 传给 `CodeBlockNode`,或者在 `MarkdownRender` 上使用 `codeBlockMonacoOptions` 统一下发。该对象会原样透传给 `useMonaco()`。
> 注意:设置 `languages` 会覆盖 `stream-monaco` 内置的 `defaultLanguages`,而不是在其基础上追加。请在数组中显式列出你需要的所有语言(包括默认语言),以免缺少语法高亮。
只有 `ts twoslash` / `vue twoslash` 代码块才会在这个文档站里显示 hover 类型信息。更推荐 hover `languages`、`themes`、`theme`、`MAX_HEIGHT`、`onDiffHunkAction` 这些具体字段。
```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>
```
> `languages` 中的每个条目都可以是 Monaco 的语言 ID,或 `stream-monaco` 文档里提到的懒加载函数(用于延迟加载语言包)。如果不是通过 `MarkdownRender`,直接在 `CodeBlockNode` 上使用 `:monaco-options="monacoOptions"` 即可。
### Diff 悬浮操作按钮
对于 diff 代码块,可以在每个 hunk 的上下分段上显示悬浮操作按钮(`Revert` / `Stage`)。这些配置同样通过 `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)
// 返回 false 可以阻止 stream-monaco 的内置编辑行为。
return false
},
} satisfies CodeBlockMonacoOptions
```
- `diffHunkActionsOnHover`:开启 hunk 悬浮按钮
- `diffHunkHoverHideDelayMs`:控制鼠标移出后悬浮按钮延迟隐藏的时间
- `onDiffHunkAction`:在默认 `revert` / `stage` 编辑执行前进行拦截
#### 完整示例
```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)
// 阻止内置编辑,方便在示例里稳定观察回调事件。
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>
```
把鼠标移到红/绿变更 hunk 区域上,就会出现 `Revert` / `Stage` 按钮。点击后会触发 `onDiffHunkAction`。
> 当前按钮文案是 `Revert` 和 `Stage`,不是 `Stash`。
>
> 在 `markstream-vue` 里,这组配置应当在首次创建编辑器时通过初始 `monacoOptions` 传入。如果你要在运行时切换它们,请重新挂载代码块,让 Monaco diff editor 按新配置重建。
@@ -0,0 +1,5 @@
# Monorepo 迁移指南
此页面为 `monorepo-migration.md` 的中文版本,帮助中文读者了解如何将解析器抽离为 `stream-markdown-parser` 包以及相应的构建与发布流程。
(内容与 `docs/monorepo-migration.md` 相同,已经翻译)
@@ -0,0 +1,271 @@
---
description: 使用解析器 API 在 markstream-vue 渲染节点之前接入 token 钩子、AST 变换与 markdown-it 定制。
---
# 解析器 — API 详解
此页从 `packages/markdown-parser` 提取常用函数、类型与便捷工具,便于构建更高级或自定义的解析流程。
## 主要函数
### `getMarkdown(msgId?, options?)`
创建配置好的 `markdown-it-ts` 实例。
参数:
- `msgId` (string,可选):实例标识
- `options` (GetMarkdownOptions,可选):配置选项
选项示例:
- `plugin`:插件数组
- `apply`:用于变更 `MarkdownIt` 实例的函数数组
- `i18n`:翻译映射或函数
- `customHtmlTags`:流式内联 HTML 中间态白名单扩展(如 `['thinking']`)
### `parseMarkdownToStructure(content, md, options?)`
将 Markdown 字符串解析为供渲染器使用的节点树(AST)。
> 提示:对于 `<thinking>` 等简单自定义标签,推荐使用内置的 `customHtmlTags` / `custom-html-tags` 白名单让解析器直接输出自定义节点;只有在需要手动重写 `content/attrs` 时再用 `preTransformTokens`。参见 [自定义组件解析示例](/zh/guide/advanced#自定义组件解析示例)。
返回:`ParsedNode[]`
### ParseOptions: `final`(流结束/最终态)
在流式渲染(SSE/AI Chat)中,解析器会主动产出一些 **“中间态(loading)节点”** 来降低闪烁,例如:
- 未闭合的块级数学公式(以 `$$` 开头但还没等到结尾 `$$`)
- 未闭合的 code fence(只收到 ``` 开始行)
这样做能让 UI 在内容尚未到齐时也能稳定渲染。但当你 **明确知道内容已经结束**(流已结束、消息已完整),应该将解析切换到最终态:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure(buffer, md, { final: true })
```
`final: true` 的效果:
- 关闭上述“中间态 loading”策略:未闭合的 `$$...` 不再产出 `math_block/loading`;
- 将 EOF 视作 code fence 的隐式闭合,避免代码块永远 loading。
### 其他有用函数
- `processTokens(tokens)` — 将原始 token 处理为可用于构建节点列表的结构
- `parseInlineTokens(tokens, content?, preToken?, options?)` — 解析 inline tokens
## 配置与工具
### `setDefaultMathOptions(options)`
设置全局 KaTeX 配置示例:
```ts
setDefaultMathOptions({
commands: ['infty', 'perp'],
escapeExclamation: true
})
```
### 工具函数
- `isMathLike(content)` — 判断文本是否可能为公式
- `findMatchingClose(src, startIdx, open, close)` — 查找匹配的闭合符号
- `parseFenceToken(token)` — 将 code fence 转为 `CodeBlockNode`
- `normalizeStandaloneBackslashT(content, options?)` — 规范化数学内容中的特殊转义
## 解析钩子(高级)
传入 `ParseOptions` 可使用以下钩子:
- `preTransformTokens`、`postTransformTokens`。
如需改造 AST,可在 `parseMarkdownToStructure` 返回后自行处理 `ParsedNode[]`,再通过 `MarkdownRender` 的 `nodes` 传入。
这些钩子也可通过 `MarkdownRender` 组件传入 `parseOptions` prop(仅当使用 `content` prop 时生效)。
### 未知 HTML 类标签(默认行为)
默认情况下,非标准的 HTML 类标签(例如 `<question>`)在完整闭合后会按原生 HTML 渲染;未闭合或格式不完整的片段会保持为**纯文本**,避免吞掉周围内容。
如果你希望某个自定义标签参与解析并产出自定义节点(以便 `setCustomComponents` 映射,并携带 attrs/content),请将其加入 `customHtmlTags`。
### ParseOptions: `requireClosingStrong`
`requireClosingStrong`(boolean,可选)控制解析器在解析 inline 内容时如何处理未闭合的 `**` 加粗分隔符。默认值:`false`(更贴合流式场景)。
- **true**:要求存在匹配的关闭 `**` 才会生成加粗(strong)节点。未闭合的 `**` 会被保留为普通文本。这是非交互渲染(例如静态页面或服务器端渲染)推荐的严格模式,可以避免像 `[**cxx](xxx)` 这类在链接文本中错误地解析出 dangling strong 的问题。
- **false**:允许中间态/未完成的 `**`(适用于编辑器的实时预览),解析器会在某些未闭合情况下仍生成临时的加粗节点。
示例 — 严格模式(建议用于非流式场景):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: true })
// 文本 `[**cxx](xxx)` 将被保留,不会创建不完整的加粗节点
```
示例 — 编辑器友好模式:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: false })
// 允许在实时预览中创建临时/中间态的加粗节点
```
### ParseOptions: `validateLink`
`validateLink`(`(url: string) => boolean`,可选)用于控制链接是否输出为 `link` 节点。
- 返回 **true**:正常输出 `link` 节点。
- 返回 **false**:降级为纯文本(仅保留链接文本)。
优先级与集成方式:
- `parseMarkdownToStructure(..., { validateLink })` 优先级最高。
- 如果未传入,解析器会复用 MarkdownIt 实例上的规则(例如通过 `md.set({ validateLink })` 或 `md.options.validateLink` 设置)。
示例 —— 拦截不安全协议,同时保留正常 https 链接:
```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" 会输出为 link 节点;"unsafe" 会降级为纯文本
```
### `$$` 异常分隔符(空公式/误触发)的解释
有些内容会出现“奇怪的 `$$`”,例如:
```md
adasd $$ dasdsa
dasdsad
```
如果把 `$$` 误识别为数学公式分隔符但内部内容为空,就可能导致数学节点一直处于 loading(渲染层没有内容可渲染)。解析器现在会将 **空内容的 `$$` 视为普通文本**,避免这类永久 loading。
### 流式内联 HTML 中间态(`html_inline`)
在流式/聊天/编辑器场景中,内联 HTML 往往以不完整的片段逐步到达。解析器采用保守的中间态策略来减少抖动,同时尽早输出可稳定渲染的 HTML。
行为说明:
- **吞并未完成标签**:当 text token 中出现但尚未闭合到 `>` 的片段(如 `<span class="a"`、结尾的 `<`、`</sp`、结尾的 `</`),会被暂存并从 AST 中移除,避免以普通文本闪烁。
- **识别已完成的开始标签**:常见 HTML 标签(保守白名单)一旦形成完整 `<tag ...>`,会被解析为 `html_inline` 节点。
- **若尚未出现显式闭合标签**,解析器会:
- 将当前 inline 里后续所有 token 视为该标签的 inner content;
- 自动在 `content/raw` 末尾补上 `</tag>`,保证片段可作为合法 HTML 渲染;
- 仍标记为 `loading: true`,并设置 `autoClosed: true` 表示来源尚未真正闭合。
- **当真实闭合标签到达后**,`autoClosed` 消失,`loading=false`。
扩展白名单:
如果需要让自定义标签(例如 `<thinking>`)也享受相同的中间态吞并与自动补闭合策略,可在创建实例时传入 `customHtmlTags`:
```ts
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
```
输出自定义节点:
如果希望这些标签直接产出自定义节点类型(便于 `setCustomComponents` 直接按 `type` 映射),可在 `ParseOptions` 中同时传入 `customHtmlTags`,或在 `MarkdownRender` 上使用 `custom-html-tags`(组件会自动透传):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
const nodes = parseMarkdownToStructure(source, md, { customHtmlTags: ['thinking'] })
```
渲染层说明:
- `HtmlInlineNode` 仅在 `loading===true` 且 `autoClosed!==true` 时显示原文。
自动补闭合的中间态会直接按 HTML 渲染(`innerHTML`),但仍保留 `loading=true` 供 UX/状态使用。
## 类型提示
导出的类型包含 `CodeBlockNode`、`ParsedNode` 等,可在 TS 中导入:
```ts
import type { CodeBlockNode, ParsedNode } from 'stream-markdown-parser'
```
## 插件与默认配置
本项目为常见解析场景提供了一些便捷插件(例如:footnote、task checkbox、sub/sup/mark 等)。注意:`emoji` 插件已从默认配置中移除——需要 Emoji 支持的用户请显式注册该插件。
可以通过多种方式添加自定义插件:
- 在 `getMarkdown` 的 `plugin` 选项中传入插件数组。
- 在 `getMarkdown` 的 `apply` 回调中直接修改返回的 `MarkdownIt` 实例。
- 在使用 `MarkdownRender` 组件时,通过 `customMarkdownIt` prop 获取并修改该渲染器使用的 `MarkdownIt` 实例。
示例 — 在组件中启用 Emoji:
```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>
```
更多示例与完整 API 请参考仓库内的 `packages/markdown-parser/README.md`。
## 示例
使用 playground 快速验证自定义标签解析:对于 `<thinking>` 等简单标签,传入 `customHtmlTags`(或组件上的 `custom-html-tags`)即可自动产出 `type: 'thinking'` 的自定义节点,再用 `setCustomComponents` 做映射。
快速测试 `parseOptions`:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('Hello **world**', md)
console.log(nodes)
// 渲染:<MarkdownRender :nodes="nodes" />
```
### 自定义组件与标签类元素
标签类自定义元素(如 `<MyWidget ...>...</MyWidget>`)往往产出复杂的 `html_block`/inline token,事后用正则或 AST 拼接会很脆。建议在解析前先提取并占位,再对占位文本做 Markdown 解析,最后渲染时把占位符还原为 Vue 组件。
推荐流程:
- 预扫描 Markdown,把自定义组件提取到映射表,并用占位符(如 `[[CUSTOM:1]]`)替换原始位置。
- 让解析器处理带占位符的内容,正常渲染 AST。
- 渲染时遇到占位符节点,再从映射表取回原始片段并挂载对应组件。
示例(简化版):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
// 1) 提取自定义标签
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) 用占位符内容做 Markdown 解析
const md = getMarkdown()
const nodes = parseMarkdownToStructure(contentWithPlaceholders, md)
// 3) 渲染时替换占位符
// if (node.type === 'text' && extracted.has(node.content)) {
// return h(CustomWrapper, { raw: extracted.get(node.content) })
// }
```
### thinking 片段
针对短小的 “thinking” 片段(如 AI 思考过程),可先抽取该片段并单独解析,再用一个独立的 `MarkdownRender` 渲染;复杂的自定义组件仍按上面的提取占位方案处理。这样避免对 AST 做易碎的字符串拼接,也便于控制自定义组件的挂载范围。
@@ -0,0 +1,10 @@
# 解析器与 API
本项目基于 `markdown-it-ts`,提供了适用于流式渲染的解析器与钩子:
- `getMarkdown()` — 创建并配置 `markdown-it-ts` 实例
- `parseMarkdownToStructure(content, md)` — 将 Markdown 转换为 Parser 节点树(用于 `MarkdownRender`)
- 支持 `preTransformTokens` / `postTransformTokens` 等钩子以定制解析行为
- 支持流式内联 HTML(`html_inline`)无抖动:吞并半截标签并自动补闭合(保留 `loading` 状态)。
(完整示例与 API 请参见英文版 `/guide/parser` 或中文版 `/zh/guide/parser`,或内部 `packages/markdown-parser/README.md`)
@@ -0,0 +1,79 @@
---
description: 优化 markstream-vue 在流式聊天、大文档、Monaco 代码块以及 Mermaid 和 KaTeX 重内容场景下的性能表现。
---
# 性能特性与建议
本渲染器针对流式与大型文档进行优化。
关键功能:
- 针对代码块的增量解析
- 最小化的 DOM 更新与内存优化
- Monaco 的流式更新
- 渐进式 Mermaid 渲染
性能建议:
- 将长文档分块流式传输,避免阻塞主线程
- 对只读代码块使用 `MarkdownCodeBlockNode` 或 `renderCodeBlocksAsPre`
- 使用 `setDefaultMathOptions` 在应用启动时设置数学渲染默认项
- 对重型节点启用 `viewportPriority`(默认开启)以延迟离屏工作
更多详细信息见 `/zh/guide/performance`。
## 包体积优化流程(维护者)
当你修改可能影响构建体积的路径(渲染器、代码块、可选 peer)时,建议在合并前执行:
- `pnpm build:analyze`:生成可视化报告(`bundle-visualizer.html`、`bundle-visualizer-tailwind.html`),确认体积变化是“落在哪个 chunk”。
- `pnpm size:check`:本地执行体积预算守卫,覆盖 `dist` 总量、最大 JS chunk,以及 `npm pack --dry-run` 的 tarball/unpacked 体积。
- 可选:通过环境变量收紧预算(`MAX_DIST_BYTES`、`MAX_JS_CHUNK_BYTES`、`MAX_PACK_TGZ_BYTES`、`MAX_PACK_UNPACKED_BYTES`)。
## 让渲染保持稳定的“逐步更新”
有些 LLM 会一次推送大量文本,导致前端表现为“卡顿一会儿再一次性显示”。想让用户始终看到稳定、连续的输出,可以:
- **保持 `typewriter` 为默认开启**,这样非代码节点会通过淡入动画平滑呈现,而不是瞬间跳出;流式追加出来的新文本片段也会补一段局部短 fade,不会把整块一起压暗。
- **调整批次渲染参数**:调低 `initialRenderBatchSize` / `renderBatchSize`(如 `12` / `24`),并设置一个 20–30 ms 的 `renderBatchDelay`,让每次渲染只插入很小的一段文本。
- **在上游做节流或拆包**:把后端一次性推送的大段文本按段落拆分,或用 50–100 ms 的防抖再更新 `content`,减少一次性 diff。
- **保留延迟可见渲染**:继续启用 `deferNodesUntilVisible` / `viewportPriority`,避免 Mermaid、Monaco 这类重型节点阻塞文字流。
- **必要时降级代码块**:在突发大块传输时暂时关闭 `codeBlockStream` 或启用 `renderCodeBlocksAsPre`,避免语法高亮抢占时间片。
这些组合可以把 DOM 工作量稳定在可控范围,哪怕服务端一次发送很多文本,用户也会感知为持续、丝滑的逐步输出。
## 虚拟化与 DOM 窗口
`MarkdownRender` 会维护一个滑动窗口,只让一部分节点常驻 DOM,从而在极长的对话或文档中保持流畅:
- `maxLiveNodes`(默认 `320`)定义了 DOM 中最多保留多少个已完全渲染的节点。减小可以省内存、增大可以保留更多回溯内容。
- `liveNodeBuffer` 控制窗口前后的超前/超后范围(默认 `60`)。如果节点高度差异巨大,可增大该值以避免快速滚动时闪烁。
- `deferNodesUntilVisible` 搭配 `viewportPriority` 使用,可以让 Mermaid、Monaco、KaTeX 等重型节点在进入视口之前保持占位骨架。
- `batchRendering` 以及 `initialRenderBatchSize`、`renderBatchSize`、`renderBatchDelay`、`renderBatchBudgetMs` 控制每一帧有多少节点从占位态切换为真实组件。该增量模式仅在关闭虚拟化(`:max-live-nodes="0"`)时生效;默认开启虚拟化时,所有节点会立即渲染,依靠窗口裁剪来限制 DOM 工作量。
示例:在保持可滚动回溯的同时降低 DOM 开销。
```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>
```
利用这些旋钮,可以把超长 AI 对话或技术文档维持在一个稳定的 CPU / 内存预算中,同时保持滚动与输入的流畅体验。
@@ -0,0 +1,36 @@
---
description: 使用本地和在线 playground 快速复现 markstream-vue 渲染问题、对比框架接入,并验证修复结果。
---
# Playground(示例演示)
仓库包含一个可交互的 `playground` 演示,用于本地调试和复现渲染问题。
## 在线演示
- 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/
## 本地运行
运行演示:
```bash
pnpm install
pnpm play
# 打开终端中显示的网址(通常为 http://localhost:5173)
```
在演示中你可以尝试:
- 实时流式 Markdown 输入
- 渐进式 Mermaid
- 自定义组件映射
- Monaco 编辑器集成示例
可直接打开的示例页面:
- `https://markstream-react.pages.dev/migration-demo`:面向 `react-markdown` 用户的 before / after 迁移演示
@@ -0,0 +1,180 @@
---
description: 通过 MarkdownRender 的 props 精细控制流式渲染、暗色主题、自定义标签、解析选项与重节点行为。
---
# 组件 Props 与选项
在集成 `markstream-vue` 时,常会需要微调流式行为、控制重节点渲染或避免 Tailwind/UnoCSS 样式冲突。本页提供对照表与排障提示。
## `MarkdownRender` 核心 props
| Prop | 类型 | 默认值 | 说明 |
| ---- | ---- | ------ | ---- |
| `content` | `string` | – | 原始 Markdown 字符串(除非提供 `nodes`,否则必填)。 |
| `nodes` | `BaseNode[]` | – | 预解析后的 AST(通常为 `parseMarkdownToStructure` 返回的 `ParsedNode[]`)。 |
| `custom-id` | `string` | – | 作用域键,可在 `setCustomComponents` 注册映射并用 `[data-custom-id="docs"]` 做样式覆盖。 |
| `is-dark` | `boolean` | `false` | 主题标记,会透传给 Mermaid/D2/KaTeX/CodeBlock,并在根容器上添加 `.dark`。 |
| `index-key` | `number \| string` | – | 内部节点 key 前缀;嵌套渲染或列表中渲染多实例时建议显式传入。 |
| `final` | `boolean` | `false` | 是否为“流结束/最终态”。开启后会关闭解析器的中间态(loading)行为,避免末尾残留分隔符(如 `$$`、未闭合 code fence)永远停留在 loading。 |
| `parse-options` | `ParseOptions` | – | Token 级钩子(`preTransformTokens`、`postTransformTokens`),仅在传入 `content` 时生效。 |
| `custom-html-tags` | `string[]` | – | 扩展流式内联 HTML 中间态白名单,并将这些标签直接输出为自定义节点(如 `type: 'thinking'`)以便 `setCustomComponents` 映射(会传给 `getMarkdown`,如 `['thinking']`)。 |
| `custom-markdown-it` | `(md: MarkdownIt) => MarkdownIt` | – | 自定义内部 MarkdownIt 实例(加插件、改配置)。 |
| `debug-performance` | `boolean` | `false` | 打印解析/渲染耗时与虚拟化统计(仅 dev)。 |
| `typewriter` | `boolean` | `true` | 控制轻量进入动画。生成静态截图或 SSR 输出时可关闭。 |
## 流式与重节点开关
| Flag | 默认值 | 功能 |
| ---- | ------ | ---- |
| `render-code-blocks-as-pre` | `false` | 将非 Mermaid/Infographic/D2 的 `code_block` 渲染为 `<pre><code>`,适合仅查看或排查 Monaco/Tailwind 样式问题。Mermaid/infographic/D2 仍会路由到各自组件,除非用 `setCustomComponents` 覆盖。 |
| `code-block-stream` | `true` | 启用流式代码块更新;关闭后会保持加载态直到完整文本就绪,避免频繁初始化 Monaco。 |
| `viewport-priority` | `true` | 优先渲染视窗内的 Mermaid/D2/Monaco/KaTeX 等重节点,延迟离屏渲染以提升交互体验。 |
| `defer-nodes-until-visible` | `true` | 启用后,重节点在接近视口前可先渲染为占位(仅在非虚拟化模式生效)。 |
## 渲染性能(虚拟化 & 分批渲染)
| Prop | 默认值 | 说明 |
| ---- | ------ | ---- |
| `max-live-nodes` | `320` | 虚拟化阈值;设为 `0` 可关闭虚拟化(全部渲染)。 |
| `live-node-buffer` | `60` | 视窗前后保留的节点数量(overscan)。 |
| `batch-rendering` | `true` | 分批渲染(仅当 `max-live-nodes <= 0` 时启用)。 |
| `initial-render-batch-size` | `40` | 初始立即渲染的节点数。 |
| `render-batch-size` | `80` | 每批渲染的节点数。 |
| `render-batch-delay` | `16` | 每批在 rAF 之后额外延迟(ms)。 |
| `render-batch-budget-ms` | `6` | 单批预算(ms),超过后会自适应缩小后续 batch。 |
| `render-batch-idle-timeout-ms` | `120` | `requestIdleCallback` 切片的超时(ms,若可用)。 |
## 代码块全局选项(由 `MarkdownRender` 下发)
这些 props 会被转发到 `CodeBlockNode` / `MarkdownCodeBlockNode`(但 **不会** 转发到 Mermaid/D2/Infographic 代码块,因为它们会路由到各自组件):
- `code-block-dark-theme`, `code-block-light-theme`
- `code-block-monaco-options`
- `code-block-min-width`, `code-block-max-width`
- `code-block-props`(额外代码块 props,例如 `showHeader`、`showFontSizeButtons`、`showTooltips`,同时保留自定义透传字段)
- `themes`(在安装 `stream-monaco` 时,会转发给其主题系统)
注意:`code-block-monaco-options` 仅作用于 Monaco 版 `CodeBlockNode`。如果你把 `code_block` 覆盖成 `MarkdownCodeBlockNode`,此时 `code-block-dark-theme` / `code-block-light-theme` 应填写 Shiki 主题名,`themes` 为需要预加载的 Shiki 主题列表。
只有 `ts twoslash` / `vue twoslash` 代码块才会在这个文档站里显示 hover 类型信息。更推荐 hover 下面对象里的字段,或者模板里的 `:code-block-monaco-options`,而不是只 hover 导入的类型名。
```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` 也可以直接走渲染器公开类型,不需要再退回 `any`:
```ts twoslash
import type { NodeRendererProps } from 'markstream-vue'
const codeBlockProps: NonNullable<NodeRendererProps['codeBlockProps']> = {
showHeader: false,
showFontSizeButtons: false,
showTooltips: false,
}
```
## 图表节点全局下发参数
如果你希望统一控制 Mermaid / D2 / Infographic 的工具栏、渐进渲染参数或交互细节,而不手动覆盖组件,可直接在 `MarkdownRender` 上传这些对象:
- `mermaid-props`:透传给 `MermaidBlockNode`
- `d2-props`:透传给 `D2BlockNode`
- `infographic-props`:透传给 `InfographicBlockNode`
对于 Mermaid 和 Infographic 围栏,调用方未传入时 `MarkdownRender` 会自动注入 `estimatedPreviewHeightPx`,用于为异步加载和重新挂载预留稳定的首屏 preview 高度。自定义 `mermaid` / `infographic` 渲染器也会收到这个 prop;如果自定义块自己渲染 preview shell,应继续转发或使用它。
示例:
```vue
<MarkdownRender
:content="md"
:mermaid-props="{ showHeader: false, renderDebounceMs: 180, previewPollDelayMs: 500 }"
:d2-props="{ progressiveIntervalMs: 450, showCopyButton: false }"
/>
```
其中 `mermaid-props` 很适合用于流式调优,常用项包括:
- `renderDebounceMs`
- `contentStableDelayMs`
- `previewPollDelayMs`
- `previewPollMaxDelayMs`
- `previewPollMaxAttempts`
- `showHeader`、`showModeToggle`、`showExportButton`、`showZoomControls` 等工具栏开关
## 代码块头部控制
可直接传给 `CodeBlockNode` / `MarkdownCodeBlockNode` / `MermaidBlockNode`,或在 `MarkdownRender` 上用 `code-block-props` 统一下发:
- `show-header`
- `show-copy-button`
- `show-expand-button`
- `show-preview-button`
- `show-collapse-button`
- `show-font-size-buttons`
- `show-tooltips`(全局控制 `LinkNode` + 代码块节点的 tooltip + Mermaid块节点的 tooltip)
更多细节请参考 `/zh/guide/codeblock-header` 及类型定义。
示例(全局默认):
```vue
<template>
<MarkdownRender
:content="md"
:code-block-props="{ showHeader: false, showFontSizeButtons: false, showTooltips: false }"
/>
</template>
```
## 示例
```vue
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
const md = '# 标题\n\n演示 props 用法。'
</script>
<template>
<MarkdownRender
:content="md"
custom-id="docs"
:viewport-priority="true"
:code-block-stream="true"
/>
</template>
```
## 样式与排障提示
1. **先引入 reset**(`modern-css-reset`、`@tailwind base`、`@unocss/reset`),再在 `@layer components` 中导入 `markstream-vue/index.css`,避免被 utilities 覆盖。详见 [Tailwind 指南](/zh/guide/tailwind)。
2. **使用 `custom-id`** 与 `[data-custom-id="docs"]` 限定覆盖范围。
3. **检查同伴 CSS** 是否导入(KaTeX),Mermaid/D2 不需要额外 CSS。
4. **查阅 [样式排查清单](/zh/guide/troubleshooting#css-looks-wrong-start-here)**,确保 reset、layer、Uno/Tailwind 配置正确。
@@ -0,0 +1,73 @@
---
description: 用最小 Vue 示例快速跑起 markstream-vue,并理解默认 CSS 行为与下一步该看哪些页面。
---
# 快速开始
示例:
```vue twoslash
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
const md: MarkdownRenderProps['content'] = `# Hello World\n\n这是 **加粗** 的文本。`
const customId: MarkdownRenderProps['customId'] = 'quick-start'
const isDark: MarkdownRenderProps['isDark'] = false
</script>
<template>
<MarkdownRender
:content="md"
:custom-id="customId"
:is-dark="isDark"
/>
</template>
```
如果你是为了看 props hover,优先 hover `MarkdownRenderProps['content']`、`MarkdownRenderProps['customId']`、`MarkdownRenderProps['isDark']`,或者上面模板里的对应 attribute。直接 hover 组件名在 Vue 片段里通常信息会更少。
说明:本包的 CSS 会限定在内部 `.markstream-vue` 容器下,以尽量减少对宿主应用全局样式的影响;主入口已经默认引入样式,正常使用 `MarkdownRender` 无需再额外 `import 'markstream-vue/index.css'`。只有当你需要精确控制 CSS 顺序时,才建议手动单独引入。
暗色变量支持两种方式:给祖先节点加 `.dark`,或给 `MarkdownRender` 传入 `:is-dark="true"`(仅对渲染器生效)。
如果使用 Nuxt 或 SSR,请用 `<client-only>` 包裹。
如果是高频 SSE / token 级流式输出,推荐在外部完成解析后传 `:nodes`,而不是每个 chunk 都让 `MarkdownRender` 重新解析整篇内容。
快速试一下:
```vue twoslash
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
const md: MarkdownRenderProps['content'] = `# Hello world
试试 Mermaid:
\`\`\`mermaid
graph LR
A-->B
\`\`\`
试试 D2:
\`\`\`d2
direction: right
Client -> API: request
API -> DB: query
DB -> API: rows
API -> Client: response
\`\`\`
`
</script>
<template>
<MarkdownRender :content="md" />
</template>
```
安装 `mermaid` 或 `@terrastruct/d2` 才能看到图表预览;未安装时会回退为源码展示。
@@ -0,0 +1,749 @@
# React 组件与 API
markstream-react 提供与 markstream-vue 相同强大的组件,但专为 React 构建。所有组件都支持 React 18+ 并包含完整的 TypeScript 支持。
根入口、`markstream-react/next`、`markstream-react/server` 现在都会产出独立声明文件。像 `NodeRendererProps`、`NodeRendererCodeBlockProps`、`NodeComponentProps`、`RenderContext`、`RenderNodeFn`、`CustomComponentMap`、`CodeBlockMonacoOptions`、`MarkdownCodeBlockNodeProps`、`ListItemNodeProps`、`HtmlPreviewFrameProps`、`TooltipProps`、`TooltipPlacement`、`LinkNodeStyleProps` 这类共享渲染器与组件类型,都可以直接从你实际使用的入口导入。
## 主组件:MarkdownRender
在 React 中渲染 markdown 内容的主要组件。
### Props
`MarkdownRender` 使用 `markstream-react` 的 `NodeRendererProps`。
#### 核心 props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `content` | `string` | - | 要渲染的 Markdown 内容 |
| `nodes` | `BaseNode[]` | - | 预解析的 AST 节点(通常为 `ParsedNode[]`) |
| `customId` | `string` | - | 作用域标识,用于 `setCustomComponents` 与样式隔离 |
| `final` | `boolean` | `false` | 标记输入结束,停止输出 streaming `loading` 节点 |
| `parseOptions` | `ParseOptions` | - | 解析选项与 token hooks(仅在传入 `content` 时生效) |
| `customHtmlTags` | `readonly string[]` | - | 作为自定义节点输出的 HTML-like 标签(如 `thinking`) |
| `customMarkdownIt` | `(md: MarkdownIt) => MarkdownIt` | - | 自定义 MarkdownIt 实例 |
| `debugPerformance` | `boolean` | `false` | 输出解析/渲染耗时与虚拟化统计(仅 dev) |
| `isDark` | `boolean` | `false` | 暗色主题标记,转发给重型节点并在根容器加 `.dark` |
| `indexKey` | `number \| string` | - | 列表渲染时的 key 前缀 |
| `typewriter` | `boolean` | `true` | 非代码节点进入动画 |
| `showTooltips` | `boolean` | `true` | 全局控制 `LinkNode` 与代码块节点 tooltip |
#### 流式与重节点开关
| 属性 | 默认值 | 描述 |
|------|---------|-------------|
| `renderCodeBlocksAsPre` | `false` | 将 `code_block` 渲染为 `<pre><code>`(Mermaid/D2/Infographic 也会随之回退) |
| `codeBlockStream` | `true` | 随内容到达流式更新代码块 |
| `viewportPriority` | `true` | 将 Monaco/Mermaid/D2/KaTeX 等重型工作延迟到接近视口时 |
| `deferNodesUntilVisible` | `true` | 重型节点先占位,接近可视区再渲染(仅非虚拟化模式) |
#### 性能(虚拟化与批次渲染)
| 属性 | 默认值 | 描述 |
|------|---------|-------------|
| `maxLiveNodes` | `320` | DOM 最大保留节点数(设为 `0` 关闭虚拟化) |
| `liveNodeBuffer` | `60` | 视窗前后 overscan 缓冲 |
| `batchRendering` | `true` | 在关闭虚拟化时启用批次渲染 |
| `initialRenderBatchSize` | `40` | 批次渲染前先渲染的节点数量 |
| `renderBatchSize` | `80` | 每个批次渲染的节点数量 |
| `renderBatchDelay` | `16` | 每次批次前的额外延迟(ms) |
| `renderBatchBudgetMs` | `6` | 自适应批次缩小前的预算(ms) |
| `renderBatchIdleTimeoutMs` | `120` | `requestIdleCallback` 超时(ms) |
#### 代码块全局配置
| 属性 | 类型 | 描述 |
|------|------|-------------|
| `codeBlockDarkTheme` | `CodeBlockMonacoTheme` | 转发到每个 `CodeBlockNode` 的 Monaco 深色主题 |
| `codeBlockLightTheme` | `CodeBlockMonacoTheme` | 转发到每个 `CodeBlockNode` 的 Monaco 浅色主题 |
| `codeBlockMonacoOptions` | `CodeBlockMonacoOptions` | 转发到 `stream-monaco` 的选项,包括 `diffHunkActionsOnHover`、`diffHunkHoverHideDelayMs`、`onDiffHunkAction` 这类 diff 悬浮操作配置 |
| `codeBlockMinWidth` | `string \| number` | 转发到 `CodeBlockNode` 的最小宽度 |
| `codeBlockMaxWidth` | `string \| number` | 转发到 `CodeBlockNode` 的最大宽度 |
| `codeBlockProps` | `NodeRendererCodeBlockProps` | 额外转发到每个代码块渲染器(`CodeBlockNode` / `MarkdownCodeBlockNode`)的 props |
| `mermaidProps` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 Mermaid 围栏和自定义 `mermaid` 渲染器的 props |
| `d2Props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 D2 围栏和自定义 `d2` 渲染器的 props |
| `infographicProps` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 infographic 围栏和自定义 `infographic` 渲染器的 props |
| `themes` | `CodeBlockMonacoTheme[]` | 转发到 `stream-monaco` 的主题列表 |
#### 重型渲染器 props 透传
`NodeRenderer` 可以直接给重型图表节点传调优参数:
```tsx
<MarkdownRender
content={markdown}
viewportPriority
mermaidProps={{
showHeader: false,
renderDebounceMs: 180,
previewPollDelayMs: 500,
}}
d2Props={{ progressiveIntervalMs: 500 }}
infographicProps={{ showHeader: false }}
/>
```
流式建议:
- 保持 `viewportPriority` 开启,避免离屏 Mermaid / Monaco / D2 在文字仍在流式更新时继续做后台工作。
- 高频 SSE 更推荐直接传 `nodes`,而不是每个 chunk 都重跑整篇 `content` 解析。
- Mermaid 常用调优项包括:`renderDebounceMs`、`contentStableDelayMs`、`previewPollDelayMs`、`previewPollMaxDelayMs`、`previewPollMaxAttempts`。
`NodeRendererCodeBlockProps` 会跟随公开的 `CodeBlockNode` props 结构(去掉 `node`),所以像 `showHeader`、`showFontSizeButtons`、`showTooltips` 这类字段都能直接获得补全,而不需要退回 `any`。
```tsx
import type { NodeRendererCodeBlockProps } from 'markstream-react'
const codeBlockProps: NodeRendererCodeBlockProps = {
showHeader: false,
showFontSizeButtons: false,
showTooltips: false,
}
```
#### 事件
| 属性 | 类型 | 描述 |
|------|------|-------------|
| `onCopy` | `(code: string) => void` | 代码块复制事件 |
| `onHandleArtifactClick` | `(payload: any) => void` | 预览/制品点击事件 |
| `onClick` | `(event: React.MouseEvent<HTMLDivElement>) => void` | 根容器点击事件 |
| `onMouseOver` | `(event: React.MouseEvent<HTMLElement>) => void` | 根容器鼠标悬停事件 |
| `onMouseOut` | `(event: React.MouseEvent<HTMLElement>) => void` | 根容器鼠标移出事件 |
### 使用
```tsx
import MarkdownRender from 'markstream-react'
function App() {
const markdown = `# Hello React!
这是 markstream-react。`
return (
<MarkdownRender
customId="docs"
content={markdown}
maxLiveNodes={150}
/>
)
}
```
## 代码块组件
### MarkdownCodeBlockNode
使用 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('代码已复制!')
}
return (
<div className="markstream-react">
<MarkdownCodeBlockNode
node={codeNode}
showCopyButton={true}
showCollapseButton={false}
onCopy={handleCopy}
/>
</div>
)
}
```
### CodeBlockNode
功能丰富的 Monaco 驱动代码块。
```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)
}
const handlePreviewCode = (artifact: any) => {
console.log('预览代码:', artifact)
}
return (
<div className="markstream-react">
<CodeBlockNode
node={codeNode}
monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
stream={true}
showCollapseButton={false}
onCopy={handleCopy}
onPreviewCode={handlePreviewCode}
/>
</div>
)
}
```
## 数学组件
### MathBlockNode
使用 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
渲染行内数学公式。
```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>
公式如下:
{' '}
<MathInlineNode node={inlineMathNode} />
</p>
</div>
)
}
```
## Mermaid 图表
### MermaidBlockNode
渐进式 Mermaid 图表渲染。
#### 快速要点
- **Tooltip 控制**:`showTooltips` 默认 `true`;设为 `false` 可关闭头部按钮 tooltip。
```tsx
import { MermaidBlockNode } from 'markstream-react'
function MermaidDiagram() {
const mermaidNode = {
type: 'code_block',
language: 'mermaid',
code: `graph TD
A[开始] --> B{能用吗?}
B -->|是| C[太好了!]`,
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>
)
}
```
事件说明:
- `onCopy(code: string)` 直接收到源码字符串(React 版本没有 `MermaidBlockEvent` 包装)。
- `onExport` / `onOpenModal` / `onToggleMode` 接收 `MermaidBlockEvent`,可用 `ev.preventDefault()` 阻止默认行为。
- `onToggleMode` 签名:`(target: 'source' | 'preview', ev)`。
- 如果是在 `MarkdownRender` / `NodeRenderer` 中统一使用,优先通过 `mermaidProps` 传这些参数。
## D2 图表
### D2BlockNode
渐进式 D2 图表渲染,失败时保留上次成功的预览。
```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>
)
}
```
## 其他节点组件
### 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}>
这是一个
{' '}
<strong>粗体</strong>
{' '}
单词。
</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: '项目 1' }] }
]
},
{
type: 'list_item',
children: [
{ type: 'paragraph', children: [{ type: 'text', content: '项目 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: '示例',
text: '点击我'
}
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: '示例图片',
title: '示例',
raw: '![示例图片](https://example.com/image.jpg)'
}
const handleClick = () => {
console.log('图片被点击!')
}
const handleLoad = () => {
console.log('图片已加载!')
}
return (
<ImageNode
node={imageNode}
onClick={handleClick}
onLoad={handleLoad}
/>
)
}
```
## 工具函数
### getMarkdown
获取配置好的 markdown-it 实例。
```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
将 markdown 字符串解析为 AST 结构。
```tsx
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# 标题\n\n这里的内容...', md)
// 与 MarkdownRender 一起使用
// <MarkdownRender nodes={nodes} />
```
### 可选 Worker(Mermaid / KaTeX)
安装后会自动加载 Mermaid/KaTeX。如需线程外解析/渲染,可注入 Worker:
```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())
```
## 自定义组件 API
### Props 接口
所有自定义节点组件都接收这些 props:
```tsx
import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'
interface NodeComponentProps<TNode = unknown> {
node: TNode // 解析后的节点数据
ctx?: RenderContext // 渲染上下文(主题、事件、开关)
renderNode?: RenderNodeFn // 子节点渲染助手
indexKey?: React.Key // 节点的唯一键
customId?: string // 作用域标识符
isDark?: boolean
typewriter?: boolean
children?: React.ReactNode
}
```
`CustomComponentMap` 就是 `setCustomComponents(...)` 接收的映射类型。
### 示例自定义组件
```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>
)
}
// 使用
function App() {
const paragraphNode = {
type: 'paragraph',
children: [
{ type: 'text', content: '自定义段落内容' }
]
}
return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
}
```
## Context + 自定义组件
可以在自定义节点组件内部使用 React Context,同时通过 `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 = `# 自定义标题
这使用了自定义标题组件。
`
return (
<ThemeContext.Provider value="dark">
<MarkdownRender customId="docs" content={markdown} />
</ThemeContext.Provider>
)
}
```
## 流式传输支持
markstream-react 支持流式 markdown 内容:
```tsx
import MarkdownRender from 'markstream-react'
import { useEffect, useState } from 'react'
function StreamingDemo() {
const [content, setContent] = useState('')
const fullContent = `# 流式传输演示
此内容正在**逐步**流式传输。
`
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 支持
markstream-react 包含完整的 TypeScript 类型定义:
```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} />
}
```
代码块相关的 props 类型(`CodeBlockNodeProps` / `MermaidBlockNodeProps` / `D2BlockNodeProps` / `InfographicBlockNodeProps` / `PreCodeNodeProps`)统一使用 `stream-markdown-parser` 的 `CodeBlockNode`(用 `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` 区分渲染器)。
## Next.js 最佳实践
优先使用专门的 Next SSR 双入口,而不是 `mounted` guard 或 `ssr: false`。
```tsx
import MarkdownRender from 'markstream-react/next'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello Next.js!" final />
}
```
如果你需要纯服务端渲染路径:
```tsx
import MarkdownRender from 'markstream-react/server'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello!" final />
}
```
完整入口模型见 [React Next SSR](/zh/guide/react-next-ssr)。
## Hooks 集成
你可以轻松地与 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('# 编辑我!')
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>
)
}
```
## 错误处理
```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>
渲染 markdown 时出错:
{error.message}
</div>
)
}
try {
return <MarkdownRender content={content} />
}
catch (err) {
setError(err as Error)
return null
}
}
```
## 下一步
- 查看 [React 快速开始](/zh/guide/react-quick-start) 获取设置示例
- 探索 [Vue 3 组件](/zh/guide/components) 获取更多组件示例(API 类似)
- 查看 [使用与 API](/zh/guide/usage) 获取高级模式
@@ -0,0 +1,268 @@
# React 安装
使用 pnpm、npm 或 yarn 安装 markstream-react。
::: tip 已经在用 react-markdown?
如果你是从 `react-markdown` 迁过来,先看[迁移指南](/zh/guide/react-markdown-migration)获取整体路径,再看[迁移 Cookbook](/zh/guide/react-markdown-migration-cookbook)查具体场景。
:::
```bash
pnpm add markstream-react
# 或
npm install markstream-react
# 或
yarn add markstream-react
```
## 要求
markstream-react 需要:
- **React 18+** 和 **ReactDOM 18+**
- **stream-markdown-parser**(作为依赖自动安装)
## 可选的对等依赖
markstream-react 通过可选的对等依赖支持各种功能。只安装你需要的功能:
| 功能 | 所需包 | 安装命令 |
|---------|------------------|-----------------|
| Shiki 代码块(`MarkdownCodeBlockNode`) | `stream-markdown` | `pnpm add stream-markdown` |
| Monaco 编辑器(完整代码块功能) | `stream-monaco` | `pnpm add stream-monaco` |
| Mermaid 图表 | `mermaid` | `pnpm add mermaid` |
| D2 图表 | `@terrastruct/d2` | `pnpm add @terrastruct/d2` |
| 数学公式渲染(KaTeX) | `katex` | `pnpm add katex` |
## 可选:启用线程外 Worker(Mermaid / KaTeX)
安装后会自动加载 Mermaid/KaTeX。如需线程外解析/渲染,可在入口注入 Worker:
```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())
```
同时记得导入必需的 CSS:
```tsx
import 'markstream-react/index.css'
import 'katex/dist/katex.min.css'
```
Monaco(`stream-monaco`)不需要单独导入 CSS。
注意:`markstream-react/index.css` 的样式被限制在内部的 `.markstream-react` 容器下,以减少全局样式冲突。`MarkdownRender` 默认在该容器内渲染。如果你单独渲染节点组件,请用 `<div className="markstream-react">...</div>` 包裹它们。
## Tailwind CSS 支持
如果你的项目使用 Tailwind,并希望避免重复注入 Tailwind utilities,请改用 Tailwind-ready 输出:
```tsx
import 'markstream-react/index.tailwind.css'
```
并在 `tailwind.config.js` 的 `content` 中加入该包导出的 class 列表:
```js
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
require('markstream-react/tailwind'),
],
}
```
这种方式可以确保 Tailwind 在清除未使用的样式时包含 markstream-react 使用的所有工具类,从而获得更小的最终打包体积。
### 快速安装:所有功能
一次性启用所有功能:
```bash
pnpm add stream-markdown stream-monaco mermaid @terrastruct/d2 katex
# 或
npm install stream-markdown stream-monaco mermaid @terrastruct/d2 katex
```
### 功能详情
#### 代码语法高亮
需要安装 `stream-markdown`:
```bash
pnpm add stream-markdown
```
`stream-markdown` 已内置 `MarkdownCodeBlockNode` 所需的 Shiki runtime。若要在 `MarkdownRender` 中使用 Shiki,请覆盖 `code_block` 渲染器(或直接使用 `MarkdownCodeBlockNode`)。
```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 编辑器
完整的代码块功能(复制按钮、字体大小控制、展开/折叠):
```bash
pnpm add stream-monaco
```
如果不安装 `stream-monaco`,代码块仍会渲染,但交互式按钮可能无法工作。
#### Mermaid 图表
渲染 Mermaid 图表:
```bash
pnpm add mermaid
```
#### D2 图表
渲染 D2 图表:
```bash
pnpm add @terrastruct/d2
```
#### KaTeX 数学公式渲染
数学公式渲染:
```bash
pnpm add katex
```
同时在应用入口文件中导入 KaTeX CSS:
```tsx
import 'katex/dist/katex.min.css'
```
## 快速测试
导入并渲染一个简单的 markdown 字符串:
```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 支持
markstream-react 使用 TypeScript 编写,并包含完整的类型定义,无需额外配置:
```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 集成
在 Next.js 中,优先使用专门的 SSR 双入口:
```tsx
import MarkdownRender from 'markstream-react/next'
import 'markstream-react/index.css'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello Next.js!" final />
}
```
如果你需要一条纯 server、只输出稳定 fallback 的路径:
```tsx
import MarkdownRender from 'markstream-react/server'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello Next.js!" final />
}
```
更完整的 App Router、Pages Router、自定义组件和验证说明见 [React Next SSR](/zh/guide/react-next-ssr)。
## Vite 集成
对于 Vite 项目,只需导入组件和样式:
```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!
这是 markstream-react 与 **Vite** 配合使用。`
return <MarkdownRender content={content} />
}
export default App
```
## Webpack 集成
对于使用 Webpack 的项目,确保你的配置处理 CSS 导入:
```js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
```
## 自定义组件设置
要使用自定义节点组件,你需要创建自定义渲染器。详情请参阅 [组件文档](/zh/guide/react-components)。
@@ -0,0 +1,301 @@
# react-markdown 迁移 Cookbook
这一页是对主迁移指南 [从 react-markdown 迁移](/zh/guide/react-markdown-migration) 的补充,专门放常见场景的 before / after 示例。
::: tip 想看可运行的例子?
可以直接打开线上 [React migration demo](https://markstream-react.pages.dev/migration-demo),在 React playground 里查看同一套 before / after 和审计输出。
:::
当你已经确定要迁移,但卡在这些具体问题时,就看这一页:
- `components.code` 怎么替换?
- `remark-gfm` 怎么办?
- `rehype-raw` 怎么迁?
- `urlTransform` 怎么替?
- `allowedElements` 怎么处理?
## 1. 纯渲染器替换
```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` 和 `components.a`
`react-markdown` 的自定义通常是围绕 HTML tag:
```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` 则是围绕节点类型:
```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`
很多 `react-markdown` 项目会覆盖 `code` 来接代码高亮:
```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>
```
在 `markstream-react` 里,一般选这三条之一:
- 继续用默认 `CodeBlockNode`
- 把 `code_block` 切到 `MarkdownCodeBlockNode`
- 设置 `renderCodeBlocksAsPre`
下面是 `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`
很多 `react-markdown` 示例会这样写:
```tsx
import remarkGfm from 'remark-gfm'
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
```
在 `markstream-react` 里,不要默认把它重新接回来。
先删掉,再观察你的真实内容是否仍然正常。表格、任务列表、删除线、代码围栏这些本来就是解析器常见覆盖场景;如果你依赖某些非常具体的插件行为,再做额外回归确认。
## 5. `rehype-raw`
典型 `react-markdown` 写法:
```tsx
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
<Markdown rehypePlugins={[rehypeRaw]}>{markdown}</Markdown>
```
到了 `markstream-react`,已经没有公开的 `rehype` 阶段。
推荐迁移规则:
- 如果你只是要标准 HTML 渲染,请直接验证内置 HTML 行为。
- 如果你接它是为了支持 `<thinking>` 之类的可信自定义标签,请改用 `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`
典型 `react-markdown` URL 改写:
```tsx
function rewriteDocsUrl(url: string) {
if (url.startsWith('/docs/'))
return `https://example.com${url}`
return url
}
<Markdown urlTransform={rewriteDocsUrl}>{markdown}</Markdown>
```
在 `markstream-react` 中,建议拆成两层:
- `parseOptions.validateLink` 负责放行/拦截
- 自定义 `link` / `image` 渲染器负责改写 URL
```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`
典型 `react-markdown` 过滤:
```tsx
<Markdown allowedElements={['h1', 'h2', 'p', 'a', 'code', 'pre']}>
{markdown}
</Markdown>
```
`markstream-react` 没有直接对等的 prop。
推荐规则:
- 如果这个 allowlist 很轻、不是关键安全边界,可以先完成渲染器迁移,再留 TODO。
- 如果这是安全或核心展示要求的一部分,就在渲染前自己过滤解析后的节点树。
也就是说,这一项通常属于“手工迁移项”,而不是自动 1:1 替换项。
## 8. 把流式能力当作第二阶段
等静态迁移稳定后,如果你有聊天 / SSE / AI 输出,再考虑升到 `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 />
}
```
这一步是可选项。不要为了“看起来更高级”而强行把纯静态场景也改成流式。
## 相关文档
- [从 react-markdown 迁移](/zh/guide/react-markdown-migration)
- [React 安装](/zh/guide/react-installation)
- [React 组件与 API](/zh/guide/react-components)
- [解析器 API](/zh/guide/parser-api)
@@ -0,0 +1,285 @@
---
description: 从 react-markdown 迁移到 markstream-react,并了解渲染器替换、插件审计与非 1:1 定制差异。
---
# 从 react-markdown 迁移
如果你当前使用 `react-markdown` 渲染 Markdown,迁移到 `markstream-react` 通常分两步:
1. 先替换渲染组件。
2. 再迁移原来依赖 `remark` / `rehype` 的自定义组件和插件逻辑。
::: warning 不一定是 1:1 替换
`markstream-react` 并不是所有 `react-markdown` 场景下的无脑直替。简单场景迁移很快,但如果你的项目挂了较长的 `remarkPlugins` / `rehypePlugins` 链,建议逐项审视后再切换。
:::
::: tip 查看在线演示
可以直接打开 [React migration demo](https://markstream-react.pages.dev/migration-demo),查看一套合成的 before / after 示例,以及一份按迁移 Skill 风格生成的审计报告。
:::
## 什么情况下继续使用 `react-markdown`
- 你只渲染完整、静态的 Markdown。
- 你的项目高度依赖 unified 生态里的 `remark` / `rehype` 插件链。
- 你需要开箱即用的 HTML allow/deny list 组件 props。
## 什么情况下更适合 `markstream-react`
- 你在渲染 AI / 聊天 / SSE 这种“内容边到边显示”的流式输出。
- 你需要渐进式 Mermaid、D2、KaTeX 或 Monaco 代码块体验。
- 你需要针对大文档做视口优先的重节点调度。
- 你希望从“字符串进、元素出”的模型升级到更适合流式更新的 AST 模型。
## 最小替换示例
```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} />
}
```
如果这一步就已经能正常渲染你的内容,后续迁移可以按需逐步做。
## 心智模型变化
- `react-markdown` 更偏 HTML tag 视角:自定义渲染通常通过 `components={{ h1, a, code }}` 完成。
- `markstream-react` 更偏 AST node 视角:自定义渲染通常通过 `setCustomComponents(...)` 和 `heading`、`link`、`code_block`、`inline_code` 这类节点类型完成。
- `react-markdown` 的核心是 `remark` -> `rehype` 流水线。
- `markstream-react` 的核心是面向流式场景的 parser AST + node renderer。
这意味着 `components.h1` 迁过来后通常不会还是 `h1`,而是变成一个 `heading` 渲染器,再根据 `node.level` 判断具体级别。
## API 对照表
| `react-markdown` | `markstream-react` | 说明 |
|---|---|---|
| `children` | `content` | Markdown 字符串通过 `content` 传入。 |
| `components` | `setCustomComponents(id?, mapping)` | `react-markdown` 的 key 是 HTML tag;`markstream-react` 的 key 是节点类型,如 `heading`、`link`、`paragraph`、`image`、`code_block`、`inline_code`。 |
| `remarkPlugins` | `customMarkdownIt` | 改用 `markdown-it` 插件,而不是 `remark` 插件。很多常见 Markdown 能力本身已经内建。 |
| `remarkPlugins={[remarkGfm]}` | 很多时候可以删掉 | 表格、任务列表、删除线、代码围栏等常见语法解析器本身已经支持。若你依赖某些边缘行为,删掉前请回归验证。 |
| `rehypePlugins` | 没有直接等价物 | `markstream-react` 没有公开的 `rehype` 阶段。请改用自定义节点渲染器、`customHtmlTags`、`parseOptions` 或对 `nodes` 做后处理。 |
| `rehypeRaw` | 通常不再需要 | HTML-like 标签本来就会被解析。如果是自定义标签,优先使用 `customHtmlTags={['thinking']}` + `setCustomComponents`。 |
| `skipHtml` | 没有直接 prop | HTML 节点默认会渲染,但内置 HTML 渲染器会拦截危险标签/属性和不安全 URL。如果你必须彻底禁用 HTML,请自行在输入阶段或节点阶段过滤。 |
| `allowedElements` / `disallowedElements` / `allowElement` | 没有直接 prop | 需要你自己过滤 `nodes` 树,或者替换指定节点的渲染器。 |
| `unwrapDisallowed` | 手动做节点过滤 | 如果你需要“去掉外层标签但保留子节点”的行为,需要在节点后处理阶段自己实现。 |
| `urlTransform` | `parseOptions.validateLink` + 自定义 `link` 渲染器 | `validateLink` 适合做放行/拦截;如果你要改写 URL,请在自定义 `link` 渲染器或节点后处理里完成。 |
## 迁移 `components`
`react-markdown` 里的自定义渲染,一般是围绕 HTML tag 写的:
```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>
```
在 `markstream-react` 里,对应思路是按节点类型改写:
```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} />
}
```
常见映射可以这样理解:
- `h1` ... `h6` -> `heading` + `node.level`
- `a` -> `link`
- `p` -> `paragraph`
- `img` -> `image`
- `code` / `pre` -> `code_block` 或 `inline_code`
- 自定义 HTML-like 标签 -> `customHtmlTags` + `setCustomComponents`
## 迁移代码高亮
很多 `react-markdown` 项目会通过覆盖 `components.code` 接第三方高亮器。
到了 `markstream-react`,一般有三种选择:
- 继续使用默认 `CodeBlockNode`,拿到 Monaco 驱动的代码块体验。
- 把 `code_block` 切到 `MarkdownCodeBlockNode`,使用更轻量的 Shiki 方案。
- 设置 `renderCodeBlocksAsPre`,直接回退到普通 `<pre><code>`。
下面是切到 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} />
}
```
## 迁移插件逻辑
### `remarkPlugins`
如果你之前用 `remarkPlugins` 做语法扩展,最接近的迁移点是 `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`
`markstream-react` 里没有直接对等的 `rehype` 阶段。
如果你原来的 `rehype` 逻辑是为了做这些事:
- HTML 树重写:迁到自定义节点渲染器或节点后处理。
- 自定义 HTML-like block:优先改成 `customHtmlTags`。
- 安全过滤:改用 `parseOptions.validateLink`、自定义渲染器或节点过滤。
## HTML、自定义标签与安全
`react-markdown` 默认通常会转义 HTML,除非你显式加上 `rehypeRaw`。
`markstream-react` 会直接解析 HTML-like 节点,而且内置 HTML 渲染器会去掉被拦截的标签、危险事件属性以及不安全的 URL。
如果你以前接 `rehypeRaw` 主要是为了支持 `<thinking>` 这类自定义标签,推荐迁移方式如下:
```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']}
/>
)
}
```
## 流式升级路径
你不需要第一天就把流式能力全部接上。比较实用的迁移路径是:
1. 先把 `react-markdown` 换成 `MarkdownRender`,继续使用 `content`。
2. 再迁移自定义渲染器和插件逻辑。
3. 如果后面要接 SSE / 聊天流输出,再把解析挪到组件外,改传 `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
/>
)
}
```
对于高频流式输出来说,这个 `nodes` 方案通常就是迁移到 `markstream-react` 的最大价值之一。
## 迁移检查清单
- 把 `<Markdown>{markdown}</Markdown>` 改成 `<MarkdownRender content={markdown} />`
- 引入 `markstream-react/index.css`
- 把 `components` 覆盖迁到 `setCustomComponents(customId, mapping)`
- 先删除已经冗余的插件,再按需补回真正还需要的能力
- 重新检查任何依赖 `rehype` 的 HTML 过滤或变换逻辑
- 如果你的应用要渲染增量输出,进一步从 `content` 升级到解析后的 `nodes`
## 相关文档
- [React 安装](/zh/guide/react-installation)
- [React 组件与 API](/zh/guide/react-components)
- [解析器 API](/zh/guide/parser-api)
@@ -0,0 +1,83 @@
# React Next SSR
`markstream-react` 现在提供两条明确的 Next.js SSR 入口:
- `markstream-react/next`:Next 官方接入入口。适合 App Router 的 Server Component 和 Pages Router 的 SSR 页面。首包先输出真实服务端 HTML,重节点在 hydration 后再增强。
- `markstream-react/server`:纯 server 入口。适合完全不进入 client component 边界、只输出稳定 fallback HTML 的路径。
样式只需要在应用壳层引入一次:
```tsx
// app/layout.tsx 或 pages/_app.tsx
import 'markstream-react/index.css'
import 'katex/dist/katex.min.css'
```
## 在 App Router 中使用 `markstream-react/next`
```tsx
import MarkdownRender from 'markstream-react/next'
const markdown = `# Hello Next.js
这个路由会先返回真实 SSR HTML。`
export default function Page() {
return <MarkdownRender content={markdown} final />
}
```
## 使用 `markstream-react/server` 做纯服务端渲染
```tsx
import MarkdownRender from 'markstream-react/server'
const markdown = `# Server entry
这条路径保持纯服务端渲染。`
export default function Page() {
return <MarkdownRender content={markdown} final />
}
```
## 自定义组件与自定义标签
如果你在服务端文件里注册 override,优先从 server 入口拿 helper,避免引入 root 入口副作用:
```tsx
import MarkdownRender, { setCustomComponents } from 'markstream-react/server'
setCustomComponents('next-ssr-demo', {
thinking: ({ children }: any) => <aside data-ssr-status="thinking-tag">{children}</aside>,
})
```
然后配合 `customId="next-ssr-demo"` 和 `customHtmlTags={['thinking']}` 渲染。
当前 SSR 验收还会显式检查:
- `customId` 作用域隔离
- custom node 的 `children` 是否进入首包 HTML
- `customHtmlTags` 的 attrs 是否保留在首包 HTML
## TypeScript
`markstream-react/next` 和 `markstream-react/server` 都会发布独立声明文件。公共 props 类型可以直接从你实际使用的入口导入:
```tsx
import type {
HtmlPreviewFrameProps,
MarkdownCodeBlockNodeProps,
NodeRendererProps,
TooltipProps,
} from 'markstream-react/next'
```
## 当前验证命令
```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 快速开始
在你的 React 项目中开始使用 markstream-react。
::: tip 正在从 react-markdown 迁移?
如果你的应用已经在使用 `react-markdown`,建议先看[迁移指南](/zh/guide/react-markdown-migration)和[迁移 Cookbook](/zh/guide/react-markdown-migration-cookbook),再开始手动改代码。
:::
## 基础设置
### 1. 安装
首先,安装包:
```bash
pnpm add markstream-react
```
### 2. 导入样式
在你的主入口文件(如 `main.tsx`、`index.tsx` 或 `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. 使用组件
```tsx
import MarkdownRender from 'markstream-react'
function App() {
const markdown = `# Hello React!
这是 **markstream-react** - 适用于 React 的流式 Markdown 渲染器。
## 功能
- 代码语法高亮
- Mermaid 图表
- 数学公式
- 还有更多功能!
\`\`\`javascript
console.log('Hello from React!')
\`\`\`
`
return (
<div>
<MarkdownRender content={markdown} />
</div>
)
}
export default App
```
## 使用 TypeScript
markstream-react 使用 TypeScript 构建,包含完整的类型定义:
```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
不要再把 `mounted` guard 或 `ssr: false` 当成主接法,Next 现在有专门的 SSR 双入口。
完整说明见 [React Next SSR](/zh/guide/react-next-ssr)。
### App Router(Next.js 14 / 15)
```tsx
import MarkdownRender from 'markstream-react/next'
export default function MarkdownPage() {
const markdown = `# Hello Next.js!
这个路由会先返回真实 SSR HTML。
`
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 />
}
```
### 纯服务端渲染
```tsx
import MarkdownRender from 'markstream-react/server'
export default function MarkdownPage() {
const markdown = `# 纯 server 入口`
return <MarkdownRender content={markdown} final />
}
```
## 启用可选功能
### 代码语法高亮
安装依赖:
```bash
pnpm add stream-markdown
```
```tsx
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
// 在 MarkdownRender 中使用 Shiki 代码块
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 图表
安装 mermaid:
```bash
pnpm add mermaid
```
导入样式(安装后会自动加载 Mermaid):
```tsx
import 'markstream-react/index.css'
function App() {
const markdown = `#### Mermaid 图表
\`\`\`mermaid
graph TD
A[开始] --> B{能用吗?}
B -->|是| C[太好了!]
B -->|否| D[继续尝试]
\`\`\``
return <MarkdownRender content={markdown} />
}
```
### D2 图表
安装 D2:
```bash
pnpm add @terrastruct/d2
```
```tsx
import 'markstream-react/index.css'
function App() {
const markdown = `#### D2 图表
\`\`\`d2
direction: right
Client -> API: request
API -> DB: query
DB -> API: rows
API -> Client: response
\`\`\``
return <MarkdownRender content={markdown} />
}
```
### 数学公式(KaTeX)
安装 katex:
```bash
pnpm add katex
```
导入样式(安装后会自动加载 KaTeX):
```tsx
import 'markstream-react/index.css'
import 'katex/dist/katex.min.css'
function App() {
const markdown = `#### 数学示例
行内数学公式:$E = mc^2$
块级数学公式:
$$
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
$$`
return <MarkdownRender content={markdown} />
}
```
## 自定义组件
你可以通过传递自定义组件映射来自定义特定节点的渲染方式:
```tsx
import MarkdownRender, { setCustomComponents } from 'markstream-react'
// 自定义标题组件
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>
)
}
// 挂载到指定的 customId 作用域
setCustomComponents('docs', { heading: CustomHeading })
function App() {
const markdown = `# 自定义标题
此标题使用自定义组件渲染。
`
return <MarkdownRender customId="docs" content={markdown} />
}
```
## 流式内容
markstream-react 支持流式 markdown 内容,适用于 AI 生成的内容:
```tsx
import MarkdownRender from 'markstream-react'
import { useState } from 'react'
function StreamingDemo() {
const [markdown, setMarkdown] = useState('')
const [isStreaming, setIsStreaming] = useState(false)
const fullText = `# 流式传输演示
此内容正在**逐步**流式传输。
## 功能
1. 渐进式渲染
2. 无布局偏移
3. 流畅动画
\`\`\`javascript
const streaming = true
console.log('流式传输已启用:', 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 ? '传输中...' : '开始流式传输'}
</button>
<MarkdownRender content={markdown} />
</div>
)
}
```
## 使用 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('# 编辑我!')
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="在这里输入 markdown..."
className="editor-input"
/>
<div className="editor-preview">
<MarkdownRender content={content} />
</div>
</div>
)
}
```
## 虚拟化列表
对于大型 markdown 文档,可以使用虚拟化:
```tsx
import MarkdownRender from 'markstream-react'
function LongDocument() {
// 你的很长的 markdown 内容
const markdown = `# 长文档`
return (
<MarkdownRender
content={markdown}
maxLiveNodes={200}
liveNodeBuffer={10}
/>
)
}
```
## 常用 Props
以下仅列出常用 props。完整列表请参考 [React Components](/guide/react-components)。
| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `content` | `string` | - | 要渲染的 Markdown 内容 |
| `nodes` | `BaseNode[]` | - | 预解析的 AST 节点(通常为 `ParsedNode[]`) |
| `customId` | `string` | - | 作用域标识符 |
| `maxLiveNodes` | `number` | `320` | 虚拟化最大节点数 |
| `liveNodeBuffer` | `number` | `60` | 过扫描缓冲区 |
| `batchRendering` | `boolean` | `true` | 启用批处理渲染 |
| `deferNodesUntilVisible` | `boolean` | `true` | 延迟重型节点 |
| `renderCodeBlocksAsPre` | `boolean` | `false` | 使用 `<pre><code>` 回退(Mermaid/D2/Infographic 也会回退) |
## 样式
默认样式限定在 `.markstream-react` 类下。你可以覆盖样式:
```css
/* 你的全局样式 */
.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;
}
```
## 使用 Tailwind CSS
```tsx
import MarkdownRender from 'markstream-react'
import 'markstream-react/index.css'
import './output.css' // 你的 Tailwind 输出
function App() {
return (
<div className="max-w-4xl mx-auto py-8">
<MarkdownRender content="# Hello Tailwind!" />
</div>
)
}
```
## 下一步
- 探索 [React 组件文档](/zh/guide/react-components) 了解所有可用组件
- 查看 [示例](/zh/guide/examples) 获取更多使用示例
- 查看 [API 参考](/zh/guide/components) 获取详细 API 文档
@@ -0,0 +1,13 @@
# 搜索
本站点启用了 VitePress 的本地搜索(Local Search)。
## 如何使用
- 点击右上角的搜索框,输入关键词即可搜索当前语言下的文档内容
- 桌面端快捷键:`⌘ K`(macOS)/ `Ctrl K`(Windows / Linux)打开搜索
## 小贴士
- 搜不到时先确认语言是否正确(中英文内容分别在 `/` 与 `/zh/`)
- 搜索对组件名/函数名很友好,建议直接搜 `MarkdownRender`、`enableMermaid`、`enableKatex` 等关键字
@@ -0,0 +1,99 @@
---
description: 解决 markstream-vue 与 Tailwind、shadcn 以及 CSS layer 顺序相关的样式覆盖问题。
---
# Tailwind 集成与样式顺序
如果你的项目使用 Tailwind 或基于 Tailwind 的组件库(如 shadcn),可能会遇到样式覆盖问题。推荐将 `markstream-vue` 的 CSS 导入置于 `@layer components { ... }` 中以控制样式优先级。
另外,`markstream-vue` 的打包 CSS 会限定在内部 `.markstream-vue` 容器下以降低全局冲突;来自 `markstream-vue/index.css` 的 Tailwind 工具类只会在该容器内生效。
示例:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
@import 'markstream-vue/index.css';
}
```
更多替代方案:使用 `prefix` 配置以避免类名冲突,或在应用层覆盖样式。
## 在 Tailwind 中避免重复生成 CSS
为了在使用 Tailwind 的项目中减少重复的 utility CSS,推荐优先使用本包提供的 "tailwind-ready" 输出,而不是完整的预编译 `index.css`。
- Tailwind v3(推荐流程):
- 在应用中引入 `index.tailwind.css`,这样只会引入组件样式而不会把 Tailwind 的工具类重复打包进去。
- 在 `tailwind.config.js` 的 `content` 中加入本包生成的类列表,使 Tailwind 能识别并避免重新生成相同的类。
示例(Tailwind v3 / `tailwind.config.js`):
```js
module.exports = {
content: [
'./src/**/*.{js,ts,vue}',
// 包安装后可以使用:require('markstream-vue/tailwind')
require('markstream-vue/tailwind'),
],
}
```
示例 CSS 导入(应用入口):
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'markstream-vue/index.tailwind.css';
```
- Tailwind v4:可以直接包含 `index.tailwind.css`,使用 v4 的扫描器发现类名,无需额外的 `tailwind.ts` 辅助文件。
- 非 Tailwind 项目:继续使用预编译好的 `index.css`:
```css
@import 'markstream-vue/index.css';
```
说明:
- 本包导出 `./tailwind` 条目(`./dist/tailwind.ts`),导出的是提取出的 class 列表。发布到 npm 后可以在 `tailwind.config.js` 中通过 `require('markstream-vue/tailwind')` 引入该列表。
- 在本地开发时也可以直接引用生成文件(例如 `./dist/tailwind.ts`)。
## UnoCSS 集成
UnoCSS 遵循相同规则:先导入 reset,再将库的 CSS 放到受控 layer 中,避免被 `utilities` 覆盖。
1. 在 `uno.config.ts` 中注入 `preflights`:
```ts
import { defineConfig, presetUno, presetWind } from 'unocss'
export default defineConfig({
presets: [presetUno(), presetWind()],
preflights: [
{
layer: 'preflights',
getCSS: () => '@import "markstream-vue/index.css";',
},
],
})
```
2. 或者手动在样式表中声明:
```css
@import '@unocss/reset/tailwind.css';
@layer components {
@import 'markstream-vue/index.css';
}
```
3. 当类名与 Uno/Tailwind 冲突时,可以在配置中设置 prefix(如 Tailwind `prefix: 'tw-'`,Uno 通过 `shortcutsPrefix` 或自定义规则),或者将 `MarkdownRender` 包裹在 `custom-id` 上下文中再进行覆盖。
调试技巧:在浏览器 DevTools 中打开 “Cascade Layers” 面板,确认 `markstream-vue` 的样式位于 utilities 之上。如果不在正确层级,请调整导入位置或 `preflights` 顺序。
@@ -0,0 +1,17 @@
# 鸣谢
感谢以下开源项目和库对本项目的帮助:
- [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)
如果你想支持该项目,请在 GitHub 上给我们点个 Star 或打开 issue 请求功能。
## Star 历史
可以在 GitHub 上查看 Star 历史图:
[![Star History Chart](https://api.star-history.com/svg?repos=Simon-He95/markstream-vue&type=Date)](https://www.star-history.com/#Simon-He95/markstream-vue&Date)
@@ -0,0 +1,41 @@
# 翻译指南
感谢你参与文档翻译工作 — 我们非常欢迎贡献!下面是推荐的翻译流程(目前我们主要支持 `zh`):
## 基本流程
1. 从 `docs/guide/` 复制英文页面到 `docs/zh/guide/` 并保持文件名一致。
2. 翻译文案,注意保持代码块、front-matter(如果存在)和链接(指向其他中文页面时请加 `/zh/` 前缀)。
3. 在本地运行 docs:
```bash
pnpm docs:dev
# 或者
pnpm docs:build
```
4. 提交 PR,并标注 `docs` 和 `i18n`(可选)。
## 链接处理
- 指向其他文档的链接在中文页面中,请使用中文路径:例如 `(/zh/guide/quick-start)`。
- 对外部链接无需更改。
## 风格与口吻
- 对技术名词保持一致翻译(例如 Monaco、Mermaid、KaTeX 等),不确定时可保留英文原词并在括号中加入译名。
- 代码示例不要改动语法,只翻译周边解释文字。
## 本地预览与校验
- 使用 `pnpm docs:dev` 本地预览,顶部右侧可以切换语言。
- 确认导航(sidebar)是否正常显示中文页面。
## PR 检查清单
- [ ] 翻译文件放置在 `docs/zh/guide/` 并保持文件名一致
- [ ] 指向其他文档的链接指向对应 `zh` 页面
- [ ] `pnpm docs:build` 可以成功构建
- [ ] PR 描述中文翻译范围与说明
如果你希望我添加 GitHub Action,自动检查 PR 翻译或链接问题,我可以继续添加这项功能。再次感谢你的贡献!
@@ -0,0 +1,127 @@
---
description: 按症状定位 markstream-vue 的 CSS 顺序、peer 依赖、SSR、自定义标签与流式性能问题。
---
# 按症状排查
当你知道“哪里不对”,但还不确定问题到底是 CSS 顺序、peer 依赖、SSR 边界、自定义标签接法,还是渲染器选择不当时,就先从这页开始。
如果你已经明确知道是哪一层出了问题,也可以直接跳到每个分支下面链接过去的深度页面。
## 先按你看到的现象分流
| 你看到的问题 | 先看这里 | 然后看 |
| --- | --- | --- |
| 样式错乱、间距不对、Tailwind 抢样式 | [样式错乱?先做这几件事](/zh/guide/troubleshooting#css-looks-wrong-start-here) | [Tailwind 集成与样式顺序](/zh/guide/tailwind) |
| `<thinking>` 这类可信标签被当成原生 HTML | [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration) 或 [自定义标签与高级组件](/zh/guide/custom-components) | [API 参考](/zh/guide/api) |
| `window is not defined` 或浏览器专属依赖在 SSR 崩掉 | [排查问题](/zh/guide/troubleshooting) | [Nuxt SSR](/zh/nuxt-ssr) |
| Mermaid、KaTeX、Monaco、D2 没有渲染出来 | [安装](/zh/guide/installation) | [渲染器与节点组件](/zh/guide/components) |
| 聊天输出卡、频繁重解析、长内容越来越慢 | [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) | [性能](/zh/guide/performance) |
| 内置节点形态不适合业务 | [覆盖内置组件](/zh/guide/component-overrides) | [渲染器与节点组件](/zh/guide/components) |
## 1. 样式看起来不对
常见现象:
- 段落、表格、列表间距很奇怪
- 代码块、图片、引用像是没吃到样式
- Tailwind / UnoCSS 把渲染器样式盖掉了
按这个顺序检查:
1. reset 是否在 `markstream-vue/index.css` 之前导入
2. 用了 Tailwind / UnoCSS 时,CSS 是否放在 `@layer components`
3. 如果启用了数学公式,是否导入了 `katex/dist/katex.min.css`
4. 如果渲染器嵌在大系统里,是否用 `custom-id` 做了作用域隔离
先从 CSS 清单开始: [排查问题](/zh/guide/troubleshooting#css-looks-wrong-start-here)
## 2. 自定义标签或自定义组件没有生效
常见现象:
- `<thinking>` 被原样输出成 HTML
- 自定义 Vue 组件一直没有渲染
- 某个页面能用,换个地方就失效
按这个顺序检查:
1. 标签是否加入了 `custom-html-tags`
2. 是否用 `setCustomComponents(customId, mapping)` 注册了映射
3. 页面渲染时是否传了匹配的 `custom-id`
4. 如果你在 VitePress 里,注册逻辑是否放在 `enhanceApp`
最适合继续看的路径:
- 文档站 / 主题场景: [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration)
- 应用内自定义标签: [自定义标签与高级组件](/zh/guide/custom-components)
## 3. SSR 报错或浏览器专属依赖崩掉
常见现象:
- `window is not defined`
- Mermaid / Monaco 本地能跑,但 SSR 环境报错
- 服务端和客户端 hydration 对不上
按这个顺序检查:
1. 先确认出问题的 peer 是否本来就只能在浏览器里运行
2. 把这部分初始化放到 client-only 边界之后
3. 让基础 `MarkdownRender` 仍然走服务端安全路径
最适合继续看的路径:
- 通用检查项: [排查问题](/zh/guide/troubleshooting)
- Nuxt 专用规则: [Nuxt SSR](/zh/nuxt-ssr)
## 4. 重型能力没有渲染出来
常见现象:
- Mermaid fence 只显示源码
- 数学公式是空白
- Monaco 代码块是空的
- D2 退回成原始文本
这类问题多数不是 parser 出错,而是下面几类原因:
- peer 依赖没装
- 必需 CSS 没导,尤其是 KaTeX
- SSR 或 worker 边界处理不对
先从 [安装](/zh/guide/installation) 开始,再去 [渲染器与节点组件](/zh/guide/components) 对照各组件的注意事项。
## 5. 流式输出或聊天界面越来越慢
常见现象:
- 每来一个 token 页面就明显抖动
- 长聊天记录越来越重
- 每次更新都在整篇重解析 Markdown
这类问题通常不是“调个样式”能解决的,而是接法需要调整:
- 把解析移到 `MarkdownRender` 外部
- 改用 `nodes` + `final`
- 重型 peers 按需启用,不要默认全开
最适合继续看的路径: [AI 聊天与流式输出](/zh/guide/ai-chat-streaming),再配合 [性能](/zh/guide/performance)
## 6. 还是拿不准?
如果你还无法判断是哪一层的问题:
1. 先在 playground 里用最小 Markdown 示例复现
2. 暂时去掉可选 peers,把问题缩小
3. 对照最接近的场景页:
`文档站与 VitePress 集成`、`AI 聊天与流式输出`、`使用与流式渲染`
如果最后看起来确实像 bug,再准备这些信息:
- 最小 Markdown 示例
- 框架 / 运行时信息
- 是否用了 Tailwind、UnoCSS、SSR
- 当前安装了哪些可选 peers
然后再使用 [排查问题](/zh/guide/troubleshooting) 里的测试页或 issue 链接。
@@ -0,0 +1,100 @@
# 排查问题
如果你还不知道问题到底属于 CSS、peer 依赖、SSR,还是自定义标签接法,建议先看 [按症状排查](/zh/guide/troubleshooting-path)。
如果出现问题,请尝试下面这些通用解决方案:
- 在 SSR 中出现 `window is not defined`:在 Nuxt 中将客户端代码包装为 `<client-only>`,在 Vite SSR 场景中用 `onMounted` 延迟初始化。
- 数学渲染失败:安装并引入 `katex`,同时在应用入口引入 `katex/dist/katex.min.css`。
- Mermaid 渲染问题:升级到 `mermaid` >= 11,检查异步渲染日志。
- 若通过 CDN `<script>` 引入 KaTeX/Mermaid:确保首次渲染前 `window.katex` / `window.mermaid` 已就绪;或在加载完成后调用一次 `setKatexLoader(() => window.katex)` / `setMermaidLoader(() => window.mermaid)` 来重置 loader。
- 性能问题:确认 `viewportPriority` 已启用,避免在单次 mount 中渲染大量重资产节点。
## 常见问题(FAQ)
- Tailwind / CSS 覆盖样式:当项目使用 Tailwind 或组件库(如 shadcn)时,Tailwind 的工具类或全局样式可能会覆盖库本身的样式。请参考 Tailwind 集成指南以了解样式导入顺序和解决策略:`/zh/guide/tailwind`。
快速修复:
- 将 `markstream-vue/index.css` 放进 `@layer components { ... }`(详见 Tailwind 页面),稳定 CSS 顺序。
- Tailwind 里设置 `prefix`(例如 `tw-`)减少与组件库 class 冲突。
- 用容器选择器或 `:deep` 把覆盖范围限制在渲染区域。
说明:`markstream-vue` 的打包 CSS 会限定在内部 `.markstream-vue` 容器下(包含主题变量与 Tailwind 工具类),因此大多数冲突通常来自 reset/导入顺序在渲染区域内的覆盖,而不是库在全局“泄漏”样式。
- 自定义样式:你可以通过覆盖 `src/index.css` 中的 CSS 变量来自定义外观(例如 `--vscode-editor-background`、`--vscode-editor-foreground`),或在你的应用样式中覆盖组件类。推荐使用 `@apply` 或将自定义样式限定到某个容器内。
- 插槽优先:如果你需要更改组件内布局,先检查组件是否暴露了插槽(例如 `header-left`、`header-right`、`loading`)。插槽提供稳健的扩展点,无需替换组件内部实现。
- 还是不够?试试 `setCustomComponents(id, mapping)` 将节点渲染器替换为你自己的 Vue 组件,详见 `Advanced` 页面的示例;记得在 SPA 中及时 `removeCustomComponents` 清理映射以避免内存泄露。
快速示例(将 `code_block` 节点替换为自定义渲染器):
```ts
import { setCustomComponents } from 'markstream-vue'
setCustomComponents('my-docs', {
code_block: MyCustomCodeBlock,
})
```
这将告知所有带有 `custom-id="my-docs"` 的 `MarkdownRender` 实例,使用 `MyCustomCodeBlock` 渲染 `code_block` 节点;更多示例请参见 `Advanced` 页面。
- 重现与提单:遇到渲染异常或报错时,请先在 `playground` 中尝试复现问题并提供最小 Markdown 示例(或使用托管的快速测试页面)。可以运行 `pnpm test` 进行本地测试以确认是否为回归问题。打开 issue 时请包含:
1. 可复现的最小 Markdown 示例(粘贴在 issue 中或放到 gist)。
2. 在 playground 的复现步骤或 `playground` 链接,以及运行环境信息(浏览器、Node 版本、Vite/Nuxt)。
3. 错误堆栈或 console 输出。
优先提供一个 `playground` 对应的复现链接;你也可以使用托管的快速测试以便快速调试:
https://markstream-vue.simonhe.me/test
如果准备好了,使用快速创建 issue 链接:
https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
额外建议:如果你可以编写一个单测或集成测试来复现 bug,请将其放入 `test/` 文件夹并在本地运行 `pnpm test`,这通常能帮助维护者快速定位并修复回归。
## 样式错乱?先做这几件事 {#css-looks-wrong-start-here}
绝大部分渲染问题都源自 CSS 重置缺失、导入顺序不对,或者 Tailwind / UnoCSS 等工具类框架覆盖组件样式。提问前请按以下清单排查:
1. **导入 reset** —— 浏览器默认 `p`、`dl`、`table`、`pre` 的 margin/padding 各不相同。先引入 reset(`modern-css-reset`、`@unocss/reset`、`@tailwind base`),再引入 `markstream-vue`:
```css
@import 'modern-css-reset';
@tailwind base;
@tailwind components;
@import 'markstream-vue/index.css';
```
2. **使用 CSS layer** —— Tailwind/UnoCSS 在 `@layer components`/`utilities` 中输出工具类,如果包的 CSS 在这些 layer 之前导入,样式可能被覆盖。把库的样式包裹在 layer 里:
```css
@layer components {
@import 'markstream-vue/index.css';
}
```
UnoCSS 也可以通过 `preflights` 注入:
```ts
import { defineConfig, presetUno } from 'unocss'
export default defineConfig({
presets: [presetUno()],
preflights: [
{
layer: 'components',
getCSS: () => '@import "markstream-vue/index.css";',
},
],
})
```
3. **确认同伴 CSS** —— KaTeX 有独立样式文件,缺失时会表现为公式空白。Mermaid 不需要额外 CSS。
4. **用 `custom-id` 限定覆盖范围** —— 集成到大型设计系统时,其他全局样式可能影响渲染器。给 `MarkdownRender` 传 `custom-id="docs"`,再通过 `[data-custom-id="docs"]` 编写覆盖,可避免污染其它页面。
如果上述步骤仍无法解决,请使用 `pnpm play` 启动 playground,准备一个只包含 CSS/Markdown 的最小示例并附带链接提 issue。
@@ -0,0 +1,61 @@
# TypeScript 使用(中文)
本库以 TypeScript 为首选语言,导出公共 API 类型。使用 `import type` 来获取节点定义以用于高级用法。
```ts twoslash
import type { BaseNode } from 'markstream-vue'
import { getMarkdown } from 'markstream-vue'
const md = getMarkdown()
const nodes: BaseNode[] = []
```
自定义组件映射类型也已经对外导出:
```ts twoslash
import type { CustomComponents } from 'markstream-vue'
const components: CustomComponents = {
thinking: {} as any,
code_block: {} as any,
}
```
## 强类型自定义组件
你可以强类型化自定义组件以接收节点特定 props,例如:
```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>
```
然后注册该组件:
```ts twoslash
import type { Component } from 'vue'
import { setCustomComponents } from 'markstream-vue'
declare const CustomCodeBlock: Component
setCustomComponents('docs', {
code_block: CustomCodeBlock,
})
```
此种方式支持类型检查与智能提示。
```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: 在 content 与 nodes 之间做出正确选择,并理解 markstream-vue 在 Vite、VitePress、Nuxt 与流式界面中的接入方式。
---
# 使用示例与 API
本页聚焦三个问题:如何在 Vite/VitePress/Nuxt 中集成、解析器如何配合渲染器、样式出错时去哪查看 reset 与 Tailwind/UnoCSS 排障指南。
## 先决定用 `content` 还是 `nodes`
| 场景 | 推荐输入 |
|------|---------|
| 文档页、静态文章、低频更新 | `content` |
| SSE、token 流式输出、AI Chat、高频增量更新 | `nodes` + `final` |
| SSR 或 Worker 里预解析完成 | `nodes` |
如果你只是在调现有能力,继续看本页和 [Props 与选项](/zh/guide/props) 就够了。只有在你要改变渲染行为时,再跳去看 [覆盖内置组件](/zh/guide/component-overrides)。
如果需要在既有设计系统里覆盖样式,务必传入 `custom-id` 并阅读 [样式排查清单](/zh/guide/troubleshooting#css-looks-wrong-start-here)。
如果你真正要做的是 AI Chat、逐 token 输出或 SSE 响应预览,建议直接走专门的 [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) 路径,不用自己拼这些页面。
如果你真正要做的是文档站或 VitePress 主题接入,建议改走 [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration),那里会把 `content`、`enhanceApp` 和 CSS 顺序串成一条路径。
## 最小渲染示例
```vue twoslash
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
const doc = '# 使用示例\n\n支持 **streaming** 渲染。'
</script>
<template>
<MarkdownRender custom-id="docs" :content="doc" />
</template>
```
```css
@import 'modern-css-reset';
@layer components {
@import 'markstream-vue/index.css';
}
```
## VitePress + 自定义标签
如果这不是一个零散示例,而是你的主要落地场景,看完这里后直接继续 [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration)。
在 VitePress 中,你只需要在 `enhanceApp` 里注册一次自定义节点组件,然后在 `MarkdownRender` 上使用 `custom-html-tags`,解析器就会自动输出对应的自定义节点。
```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
<!-- 在 VitePress 页面里 -->
<MarkdownRender
custom-id="docs"
:custom-html-tags="['thinking']"
:content="source"
/>
```
提示:像 `<question>` 这类未知的 HTML 类标签在完整闭合后会按原生 HTML 渲染。未闭合或格式不完整的片段会保持为纯文本,避免吞掉附近的 Markdown。只有当你希望它参与解析并产出自定义节点(type + attrs/content)时,才需要加入 `custom-html-tags`。
## 解析流程
```ts twoslash
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# 标题', md)
// 将 nodes 传入 <MarkdownRender :nodes="nodes" />
```
- `getMarkdown(msgId?, options?)` 返回预设配置的 `markdown-it-ts`。
- `parseMarkdownToStructure(content, md)` 将 Markdown 字符串转为渲染器使用的 AST。
- 可搭配 `setCustomComponents(id?, mapping)` 为特定 `custom-id` 替换节点渲染器。
## 流式推荐用法
`content` 模式适合低频更新或一次性渲染;如果你在做 AI Chat、SSE、逐 token 输出,推荐把解析放到外部,然后用 `:nodes` + `:final` 驱动渲染器。这样可以减少整篇重解析、降低重绘次数,也更容易把解析工作放到 Worker 或独立状态层。
```ts twoslash
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown('chat-message')
declare const streamedText: string
declare const final: boolean
const nodes = parseMarkdownToStructure(streamedText, md, { final })
```
```vue twoslash
<script setup lang="ts">
import type { BaseNode } from 'markstream-vue'
import MarkdownRender from 'markstream-vue'
const nodes: BaseNode[] = []
const final = false
</script>
<template>
<MarkdownRender :nodes="nodes" :final="final" />
</template>
```
如果你想要一条从安装、接法、性能到排障都串好的完整路径,继续看 [AI 聊天与流式输出](/zh/guide/ai-chat-streaming)。
## 组件速览
完整说明参考 [组件与节点渲染器](/zh/guide/components):
- `CodeBlockNode` — Monaco 代码块(需要安装 `stream-monaco`)。
- `MarkdownCodeBlockNode` — 基于 Shiki,适合轻量场景。
- `MermaidBlockNode` — 需要 `mermaid` ≥ 11(无需额外 CSS)。
- `D2BlockNode` — 需要 `@terrastruct/d2`(无需额外 CSS)。
- `ImageNode` — 通过 `click`/`load`/`error` 事件接管图片预览。
## 样式提醒
1. **先 reset** —— `modern-css-reset`、`@tailwind base` 或 `@unocss/reset`,之后再导入库的 CSS。
2. **使用 CSS layer** —— Tailwind/UnoCSS 项目请在 `@layer components { ... }` 中导入 `markstream-vue/index.css`。
3. **处理 Uno/Tailwind 冲突** —— 参见 [Tailwind 指南](/zh/guide/tailwind)(包含 UnoCSS 示例)。
4. **同伴 CSS** —— KaTeX 需要对应的 CSS;Mermaid/D2 不需要。Monaco 不需要额外导入 CSS。
## CSS 作用域(重要)
本包打包出来的 CSS 会限定在内部 `.markstream-vue` 容器下(包含 Tailwind 工具类与主题变量),以尽量降低全局冲突风险。
- 使用 `MarkdownRender` 时会自动包含该容器,无需额外处理。
- 如果你单独使用导出的节点组件(例如 `PreCodeNode`、`FootnoteNode`),请在外层包一层容器:
```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>
```
若仍无法解决,请运行 `pnpm play` 在 playground 中复现,并附带链接到 issue 中。
@@ -0,0 +1,148 @@
---
description: 将 markstream-vue 接入 VitePress 与文档站,覆盖 content 模式、enhanceApp 注册、可信标签接法与 CSS 顺序。
---
# 文档站与 VitePress 集成
当你在做文档站、知识库、官网内容页,或者任何“Markdown 主要作为稳定页面内容来展示,而不是像聊天那样持续流式变化”的场景时,就走这条路径。
这页的目标是一次解决 VitePress 用户最常见的几个连环问题:
- 我该用 `content` 还是 `nodes`
- `MarkdownRender` 和自定义节点该注册到哪里
- `enhanceApp` 应该怎么用
- `markstream-vue/index.css` 该放在哪
- `thinking` 这类可信标签在文档页里怎么接
## 1. 除非你已经拥有 AST,否则优先用 `content`
对文档站来说,`content` 通常就是更合适的默认值:
| 场景 | 推荐输入 |
| --- | --- |
| Markdown 来自页面内容、CMS 字段或普通文档页 | `content` |
| 你已经在服务端或内容流水线里预解析好了 | `nodes` |
| 你做的是实时流式输出或 AI 聊天 | 改走 [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) |
## 2. 先装一套适合文档站的最小依赖
```bash
pnpm add markstream-vue stream-markdown
```
这套组合很适合作为文档站默认值:基础 Markdown 渲染,加上轻量代码高亮。
只有真的需要时再继续补:
- `mermaid`:Mermaid 图表
- `katex`:数学公式
- `stream-monaco`:Monaco 代码块
## 3. 在 `enhanceApp` 中注册渲染器和自定义节点
对 VitePress 来说,最稳妥的全局接入位置通常是 `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,
})
},
}
```
这样做的好处:
- Markdown 页面里可以直接使用 `MarkdownRender`,不用每页单独导入
- 文档站专用覆盖都能收口在 `custom-id="docs"` 下
- 主题层配置集中管理,不会散落在各个页面里
## 4. 在文档页里渲染 Markdown
```md
<script setup lang="ts">
const source = `
# Hello docs
<thinking>
这个块会被自定义 Vue 组件接管渲染。
</thinking>
`
</script>
<MarkdownRender
custom-id="docs"
:content="source"
:custom-html-tags="['thinking']"
/>
```
这里有三个关键点:
- `custom-id="docs"` 用来把覆盖范围限制在文档站
- `custom-html-tags` 告诉解析器把标签变成真正的自定义节点,而不是原样输出 HTML
- 对普通文档内容来说,`content` 比手动维护 `nodes` 更简单
## 5. 把 CSS 放到可预期的位置
如果你的文档主题有自己的样式文件,推荐在主题样式里引入 `markstream-vue/index.css`,并确保它位于 reset 或基础层之后。
```css
/* docs/.vitepress/theme/style.css */
@layer components {
@import 'markstream-vue/index.css';
}
[data-custom-id='docs'] .prose {
max-width: 72ch;
}
```
实用规则:
- 站点有 reset,就先加载 reset,再加载这里
- 如果你用了 Tailwind / UnoCSS,把它放进 `@layer components`
- 如果你启用了 KaTeX,还要额外引入 `katex/dist/katex.min.css`
深入看: [Tailwind 集成与样式顺序](/zh/guide/tailwind)
## 6. `thinking` 这类可信标签的推荐接法
对文档页来说,推荐流程仍然是:
1. 用 `custom-html-tags` 声明标签
2. 用 `setCustomComponents('docs', mapping)` 注册对应渲染器
3. 让页面渲染器始终带着 `custom-id="docs"`
大多数情况下这就够了。只有当源格式本身需要改 AST,而不只是换个组件渲染时,才需要 parser hooks。
深入看: [自定义标签与高级组件](/zh/guide/custom-components)、[API 参考](/zh/guide/api)
## 7. 文档站里最常见的几个坑
- 自定义标签被当成原生 HTML:少了 `custom-html-tags`
- 组件一直没生效:没有在 `enhanceApp` 注册,或映射 key 和标签名不一致
- 样式错乱:CSS 导入顺序不对,或者被 utility layer 覆盖
- 覆盖影响到别的页面:用了全局映射,没有通过 `custom-id` 限定作用域
- Mermaid 或 Monaco 在 SSR 类环境里异常:继续看对应专题页和客户端边界说明
如果页面布局或样式还是不对,先从这里排: [故障排除](/zh/guide/troubleshooting#css-looks-wrong-start-here)
## 下一步继续看
- [安装](/zh/guide/installation):选 peer 依赖
- [使用与流式渲染](/zh/guide/usage):理解 `content` vs `nodes`
- [自定义标签与高级组件](/zh/guide/custom-components):处理嵌套标签和更复杂场景
- [Tailwind 集成与样式顺序](/zh/guide/tailwind):处理 utility CSS 技术栈
- [故障排除](/zh/guide/troubleshooting):继续排样式、peers 和 SSR
@@ -0,0 +1,641 @@
# Vue 2 组件与 API
markstream-vue2 提供与 markstream-vue 相同强大的组件,但专为 Vue 2 构建。所有组件都兼容 Vue 2.6+(配合 `@vue/composition-api`)和 Vue 2.7+。
## TypeScript 类型导出
`markstream-vue2` 同步导出渲染器与组件 props 类型:
```ts
import type {
CodeBlockNodeProps,
D2BlockNodeProps,
InfographicBlockNodeProps,
MermaidBlockNodeProps,
NodeRendererProps,
PreCodeNodeProps,
} from 'markstream-vue2'
import type { CodeBlockNode } from 'stream-markdown-parser'
```
说明:
- `NodeRendererProps` 对应 `<MarkdownRender>` props。
- `CodeBlockNodeProps` / `MermaidBlockNodeProps` / `D2BlockNodeProps` / `InfographicBlockNodeProps` / `PreCodeNodeProps` 的 `node` 统一为 `CodeBlockNode`(用 `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` 区分渲染器)。
## 主组件:MarkdownRender
在 Vue 2 中渲染 markdown 内容的主要组件。
### Props
`MarkdownRender` 在 Vue 2 中的 props 与 Vue 3 版本一致;模板中使用 kebab-case。
#### 核心 props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `content` | `string` | - | 要渲染的 Markdown 内容 |
| `nodes` | `BaseNode[]` | - | 预解析的 AST 节点(通常为 `ParsedNode[]`) |
| `custom-id` | `string` | - | 作用域标识,用于 `setCustomComponents` 与 `[data-custom-id="..."]` |
| `final` | `boolean` | `false` | 标记输入结束,停止输出 streaming `loading` 节点 |
| `parse-options` | `ParseOptions` | - | 解析选项与 token hooks(仅在传入 `content` 时生效) |
| `custom-html-tags` | `string[]` | - | 作为自定义节点输出的 HTML-like 标签(如 `thinking`) |
| `custom-markdown-it` | `(md: MarkdownIt) => MarkdownIt` | - | 自定义 MarkdownIt 实例 |
| `debug-performance` | `boolean` | `false` | 输出解析/渲染耗时与虚拟化统计(仅 dev) |
| `is-dark` | `boolean` | `false` | 暗色主题标记,转发给重型节点并在根容器加 `.dark` |
| `index-key` | `number \| string` | - | 列表渲染时的 key 前缀 |
| `typewriter` | `boolean` | `true` | 非代码节点进入动画 |
| `show-tooltips` | `boolean` | `true` | 全局控制 `LinkNode` 与代码块节点 tooltip |
#### 流式与重节点开关
| 属性 | 默认值 | 描述 |
|------|---------|-------------|
| `render-code-blocks-as-pre` | `false` | 将非 Mermaid/Infographic/D2 的 `code_block` 渲染为 `<pre><code>` |
| `code-block-stream` | `true` | 随内容到达流式更新代码块 |
| `viewport-priority` | `true` | 将 Monaco/Mermaid/D2/KaTeX 等重型工作延迟到接近视口时 |
| `defer-nodes-until-visible` | `true` | 重型节点先占位,接近可视区再渲染(仅非虚拟化模式) |
#### 性能(虚拟化与批次渲染)
| 属性 | 默认值 | 描述 |
|------|---------|-------------|
| `max-live-nodes` | `320` | DOM 最大保留节点数(设为 `0` 关闭虚拟化) |
| `live-node-buffer` | `60` | 视窗前后 overscan 缓冲 |
| `batch-rendering` | `true` | 在关闭虚拟化时启用批次渲染 |
| `initial-render-batch-size` | `40` | 批次渲染前先渲染的节点数量 |
| `render-batch-size` | `80` | 每个批次渲染的节点数量 |
| `render-batch-delay` | `16` | 每次批次前的额外延迟(ms) |
| `render-batch-budget-ms` | `6` | 自适应批次缩小前的预算(ms) |
| `render-batch-idle-timeout-ms` | `120` | `requestIdleCallback` 超时(ms) |
#### 代码块全局配置
| 属性 | 类型 | 描述 |
|------|------|-------------|
| `code-block-dark-theme` | `any` | 转发到每个 `CodeBlockNode` 的 Monaco 深色主题 |
| `code-block-light-theme` | `any` | 转发到每个 `CodeBlockNode` 的 Monaco 浅色主题 |
| `code-block-monaco-options` | `Record<string, any>` | 转发到 `stream-monaco` 的选项,包括 `diffHunkActionsOnHover`、`diffHunkHoverHideDelayMs`、`onDiffHunkAction` 这类 diff 悬浮操作配置 |
| `code-block-min-width` | `string \| number` | 转发到 `CodeBlockNode` 的最小宽度 |
| `code-block-max-width` | `string \| number` | 转发到 `CodeBlockNode` 的最大宽度 |
| `code-block-props` | `Record<string, any>` | 额外转发到每个 `CodeBlockNode` 的 props |
| `mermaid-props` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 Mermaid 围栏和自定义 `mermaid` 渲染器的 props |
| `d2-props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 D2 围栏和自定义 `d2` 渲染器的 props |
| `infographic-props` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 infographic 围栏和自定义 `infographic` 渲染器的 props |
| `themes` | `string[]` | 转发到 `stream-monaco` 的主题列表 |
#### 重型渲染器 props 透传
`MarkdownRender` 可以直接给重型图表节点传调优参数:
```vue
<MarkdownRender
:content="markdown"
:viewport-priority="true"
:mermaid-props="{
showHeader: false,
renderDebounceMs: 180,
previewPollDelayMs: 500
}"
:d2-props="{ progressiveIntervalMs: 500 }"
:infographic-props="{ showHeader: false }"
/>
```
流式建议:
- 保持 `viewport-priority` 开启,避免离屏 Mermaid / Monaco / D2 在文字仍在流式更新时继续做后台工作。
- 高频 SSE 更推荐直接传 `nodes`,而不是每个 chunk 都重跑整篇 `content` 解析。
- Mermaid 常用调优项包括:`renderDebounceMs`、`contentStableDelayMs`、`previewPollDelayMs`、`previewPollMaxDelayMs`、`previewPollMaxAttempts`。
#### 事件
- `@copy`, `@handleArtifactClick`, `@click`, `@mouseover`, `@mouseout`
### 使用
```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>
```
## 代码块组件
### MarkdownCodeBlockNode
使用 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('代码已复制!')
}
}
}
</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
功能丰富的 Monaco 驱动代码块。
```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)
},
handlePreviewCode(artifact) {
console.log('预览代码:', 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>
```
## 数学组件
### MathBlockNode
使用 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
渲染行内数学公式。
```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>
公式如下:
<MathInlineNode :node="inlineMathNode" />
</p>
</div>
</template>
```
## Mermaid 图表
### MermaidBlockNode
渐进式 Mermaid 图表渲染。
#### 快速要点
- **Tooltip 控制**:`showTooltips` 默认 `true`;设为 `false` 可关闭头部按钮 tooltip。
```vue
<script>
import { MermaidBlockNode } from 'markstream-vue2'
export default {
components: { MermaidBlockNode },
data() {
return {
mermaidNode: {
type: 'code_block',
language: 'mermaid',
code: `graph TD
A[开始] --> B{能用吗?}
B -->|是| C[太好了!]`,
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"
@export="onExport"
/>
</div>
</template>
```
常用流式调优 props:
- `render-debounce-ms`
- `content-stable-delay-ms`
- `preview-poll-delay-ms`
- `preview-poll-max-delay-ms`
- `preview-poll-max-attempts`
## D2 图表
### D2BlockNode
渐进式 D2 图表渲染,失败时保留上次成功的预览。
```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>
```
## 工具函数
### setCustomComponents
为特定的 markdown 节点注册自定义节点渲染器。
```js
import { setCustomComponents } from 'markstream-vue2'
// 定义自定义组件
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))
}
}
// 全局注册
setCustomComponents('docs', {
heading: CustomHeading
})
```
### getMarkdown
获取配置好的 markdown-it 实例。
```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
将 markdown 字符串解析为 AST 结构。
```js
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue2'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# 标题\n\n这里的内容...', md)
// 与 MarkdownRender 一起使用
// <MarkdownRender :nodes="nodes" />
```
### enableKatex / enableMermaid / enableD2
(重新)启用 KaTeX、Mermaid、D2 的功能加载器。默认 loader 已经启用,仅在你手动关闭过或需要自定义 loader(如 CDN 版本)时调用。
```js
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue2'
// 启用 KaTeX loader
enableKatex()
// 启用 Mermaid loader
enableMermaid()
// 启用 D2 loader
enableD2()
```
## 自定义组件 API
### Props 接口
所有自定义节点组件都接收这些 props:
```ts
interface NodeComponentProps {
node: ParsedNode // 解析后的节点数据
indexKey: number | string // 节点的唯一键
customId?: string // 用于作用域的自定义 ID
isDark?: boolean // 主题标记(来自 MarkdownRender)
typewriter?: boolean // 进入动画标记(仅非 code 节点)
loading?: boolean // 流式中间态(来自 node.loading)
}
```
### 示例自定义组件
```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>
<!-- 处理其他节点类型... -->
</template>
</slot>
</component>
</template>
<style scoped>
.custom-paragraph {
line-height: 1.7;
color: #333;
}
</style>
```
## 流式传输支持
markstream-vue2 支持流式 markdown 内容,节点上带有 `loading` 状态:
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
components: { MarkdownRender },
data() {
return {
streamingContent: '',
fullContent: `# 流式传输演示
此内容正在**逐步**流式传输。
\`\`\`javascript
console.log('流式传输中...')
\`\`\`
`
}
},
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 支持
markstream-vue2 包含完整的 TypeScript 类型定义。对于 Vue 2.6.x,配置你的 `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["@vue/composition-api", "markstream-vue2"],
"moduleResolution": "node",
"esModuleInterop": true
}
}
```
对于 Vue 2.7+,类型会自动包含:
```ts
import type { ParsedNode } from 'markstream-vue2'
import MarkdownRender from 'markstream-vue2'
// 你的组件具有适当的类型
import { defineComponent, ref } from 'vue'
export default defineComponent({
components: { MarkdownRender },
setup() {
const markdown = ref('# Hello')
const nodes = ref<ParsedNode[]>([])
return { markdown, nodes }
}
})
```
## 与 Vue 3 版本的差异
Vue 2 版本与 Vue 3 版本保持 API 兼容,但需要注意以下几点:
1. **Composition API**:Vue 2.6.x 需要 `@vue/composition-api`,Vue 2.7+ 内置支持
2. **插槽**:使用 Vue 2 的作用域插槽语法
3. **事件名称**:在模板中使用短横线命名的事件名称
4. **v-model**:无需更改,使用方式相同
## 下一步
- 查看 [Vue 2 快速开始](/zh/guide/vue2-quick-start) 获取设置示例
- 探索 [Vue 3 组件](/zh/guide/components) 获取更多组件示例(API 相同)
- 查看 [使用与 API](/zh/guide/usage) 获取高级模式
@@ -0,0 +1,479 @@
# Vue 2 安装
使用 pnpm、npm 或 yarn 安装 markstream-vue2。
```bash
pnpm add markstream-vue2
# 或
npm install markstream-vue2
# 或
yarn add markstream-vue2
```
## 要求
markstream-vue2 需要:
- **Vue 2.6.14+**(推荐使用 Vue 2.7 以获得更好的 TypeScript 支持)
- **@vue/composition-api**(如果使用 Vue 2.6.x)
## Composition API 版本兼容(Vue 2.6 / 2.7 / 3.x)
| Vue 版本 | Composition API 状态 | 需要安装 | 如何导入 |
|---------|----------------------|---------|---------|
| **2.6.x** | 不内置 | `@vue/composition-api` | `import { ref, computed, defineComponent } from '@vue/composition-api'` |
| **2.7.x** | 内置 | 无 | `import { ref, computed, defineComponent } from 'vue'` |
| **3.x** | 内置 | 无 | `import { ref, computed, defineComponent } from 'vue'` |
注意:
- **Vue 2.6.x** 必须安装并 **Vue.use(@vue/composition-api)**,否则会出现运行时报错。
- **Vue 2.7.x** **不需要** 安装 `@vue/composition-api`(内置)。
- **Vue 3.x** 请使用 **markstream-vue**(不是 markstream-vue2)。
## 按版本快速开始(依赖 + 入口)
### Vue 2.6.x
依赖:
```bash
pnpm add markstream-vue2 vue@2.6.14 vue-template-compiler@2.6.14 @vue/composition-api
```
入口文件:
```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:
```ts
import { defineComponent, ref } from '@vue/composition-api'
```
仓库示例:
- `playground-vue2-cli`(Vue 2.6 + Vue CLI / Webpack 4)
路径与启动命令:
```bash
pnpm -C playground-vue2-cli dev
```
也可以在仓库根目录直接运行:
```bash
pnpm play:vue2-cli
```
### Vue 2.7.x
依赖:
```bash
pnpm add markstream-vue2 vue@2.7.16 vue-template-compiler@2.7.16
```
入口文件:
```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:
```ts
import { defineComponent, ref } from 'vue'
```
仓库示例:
- `playground-vue2`(Vue 2.7 + Vite)
路径与启动命令:
```bash
pnpm -C playground-vue2 dev
```
也可以在仓库根目录直接运行:
```bash
pnpm play:vue2
```
### Vue 3.x(使用 markstream-vue)
依赖:
```bash
pnpm add markstream-vue vue@^3
```
入口文件:
```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')
```
如果你的工作区同时安装了 Vue 3,请确保 `vue-demi` 指向 Vue 2:
```bash
pnpm vue-demi-switch 2
```
如果无法运行 `vue-demi-switch`(或希望只在某个应用里处理),可以在打包配置里强制把 `vue-demi` 指向 Vue 2 版本,避免出现 `defineComponent is not a function`:
```js
// vue.config.js / webpack 配置
module.exports = {
configureWebpack: {
resolve: {
alias: {
'vue-demi$': 'vue-demi/lib/v2/index.cjs',
},
},
},
}
```
## 可选的对等依赖
markstream-vue2 通过可选的对等依赖支持各种功能。只安装你需要的功能:
| 功能 | 所需包 | 安装命令 |
|---------|------------------|-----------------|
| Shiki 代码块(`MarkdownCodeBlockNode`) | `stream-markdown` | `pnpm add stream-markdown` |
| Monaco 编辑器(完整代码块功能) | `stream-monaco` | `pnpm add stream-monaco` |
| Mermaid 图表 | `mermaid` | `pnpm add mermaid` |
| D2 图表 | `@terrastruct/d2` | `pnpm add @terrastruct/d2` |
| 数学公式渲染(KaTeX) | `katex` | `pnpm add katex` |
## Vue 2.6.x 设置
如果你使用的是 Vue 2.6.x,需要安装并配置 `@vue/composition-api`:
```bash
pnpm add @vue/composition-api
```
然后在你的应用入口文件中:
```ts
import VueCompositionAPI from '@vue/composition-api'
import Vue from 'vue'
Vue.use(VueCompositionAPI)
```
在 Vue 2.6 组件里使用 Composition API:
```ts
import { defineComponent, ref } from '@vue/composition-api'
```
## Vue 2.7.x 设置(无需插件)
Vue 2.7 已内置 Composition API,**不要** 安装 `@vue/composition-api`:
```ts
import { defineComponent, ref } from 'vue'
```
## 功能加载器(Mermaid / KaTeX / D2)
安装可选对等依赖后,默认 loader 已经启用。仅当你之前手动关闭,或需要自定义 loader(例如使用 CDN 版本)时才需要调用:
```ts
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue2'
// 可选:重新启用或覆盖 loader
enableMermaid()
enableKatex()
enableD2()
```
同时记得导入必需的 CSS(按需使用):
```ts
import 'markstream-vue2/index.css'
import 'katex/dist/katex.min.css'
```
Monaco(`stream-monaco`)不需要单独导入 CSS。
注意:`markstream-vue2/index.css` 的样式被限制在内部的 `.markstream-vue2` 容器下,以减少全局样式冲突。`MarkdownRender` 默认在该容器内渲染。如果你单独渲染节点组件,请用 `<div class="markstream-vue2">...</div>` 包裹它们。
## Vue CLI(Webpack 4)特别说明
Vue CLI 4 默认使用 **Webpack 4**,它不支持 `package.json#exports`,因此以下写法会在 Webpack 4 下解析失败:
- `import 'markstream-vue2/index.css'`
- `import('mermaid')`
- Vite 风格的 worker 导入:`?worker` / `?worker&inline`
推荐做法(可直接参考 `playground-vue2-cli`):
1) CSS 改用真实路径(不依赖 `exports`):
```ts
import 'markstream-vue2/dist/index.css'
```
2) Mermaid 推荐使用 CDN 的全局构建(避免 Webpack 4 解析 `exports` / ESM 依赖链):
```html
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
```
3) KaTeX / Mermaid worker 推荐使用 CDN worker(避免 Webpack 4 worker 限制):
```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)
```
如果你希望继续使用 `markstream-vue2/index.css` 这类导入路径,需要在 `vue.config.js` 里做 `resolve.alias` 映射(见 `playground-vue2-cli/vue.config.js`)。
### 代码块:推荐用 stream-markdown 覆盖
在 Webpack 4 下,Monaco 集成(`stream-monaco`)很容易踩到边角问题(worker 定位、ESM 入口、service 注册、以及 Shiki/TextMate regex engine 初始化等)。如果你的目标是“让代码块正确高亮渲染”,更稳妥的方案是:
1) 安装 Shiki 渲染依赖:
```bash
pnpm add stream-markdown
```
2) 覆盖 `code_block`,直接使用 `MarkdownCodeBlockNode`(或你自己的组件):
```ts
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
setCustomComponents({ code_block: MarkdownCodeBlockNode })
```
3) Webpack 配置踩坑:
- 如果你使用 `webpack.IgnorePlugin` 忽略可选依赖,确保不要把 `stream-markdown` 忽略掉。
- `stream-markdown` 是 ESM-only 包,在 CJS 的 `vue.config.js` 里即便已安装,`require.resolve('stream-markdown')` 也可能失败;建议用文件系统兜底定位 `node_modules/stream-markdown` 并 alias 到 `dist/index.js`。可参考 `playground-vue2-cli/vue.config.js`。
另外,如果你的项目是 monorepo/workspace(pnpm 常见),请确保整个应用只使用 **同一份 Vue 2 runtime**。否则会出现类似:
`provide() can only be used inside setup()` / `onMounted is called when there is no active component instance` 的运行时警告(本质是重复 Vue 实例导致 Composition API 上下文不一致)。解决方式是在 Webpack 中固定 `vue$` 指向项目内的 Vue 2 runtime(见 `playground-vue2-cli/vue.config.js`)。
## 常见运行时报错排查
### `defineComponent is not a function`
原因:`vue-demi` 处于 Vue 3 模式,但应用实际运行在 Vue 2.x。
解决(任选其一):
- 运行 `pnpm vue-demi-switch 2`
- 或在打包配置里把 `vue-demi$` alias 到 `vue-demi/lib/v2/index.cjs`(见上文)
### `provide() can only be used inside setup()` / `onMounted is called when there is no active component instance`
原因:monorepo/pnpm 工作区出现重复的 Vue 2 运行时实例。
解决:确保全项目只使用 **一个 Vue 2 实例**:
- 在打包配置里将 `vue$` alias 到应用自身的 Vue 2 运行时(参考 `playground-vue2-cli/vue.config.js`)
- 在 pnpm workspace 中用 `overrides` 固定 `playground-vue2-cli>vue` 为 Vue 2 版本
### `Vue packages version mismatch`
原因:`vue` 与 `vue-template-compiler` 版本不一致。
解决:对齐版本(例如都用 `2.6.14` 或都用 `2.7.16`),pnpm 可以用 `packageExtensions` / `overrides` 处理。
### `Cannot read properties of undefined (reading 'props')`
原因:Vue 2.6 + Composition API 缺少 `_setupProxy` patch,或 Composition API 未安装。
解决:
- 确保已安装 `@vue/composition-api` 并在入口执行 `Vue.use(VueCompositionAPI)`
- 升级到本仓库最新的 `markstream-vue2` 构建(已对 Vue 2.6 补丁 `_setupProxy`)
## Tailwind CSS 支持
如果你的项目使用 Tailwind,并希望避免重复注入 Tailwind utilities,请改用 Tailwind-ready 输出:
```ts
import 'markstream-vue2/index.tailwind.css'
```
并在 `tailwind.config.js` 的 `content` 中加入该包导出的 class 列表:
```js
module.exports = {
content: [
'./src/**/*.{js,ts,vue}',
require('markstream-vue2/tailwind'),
],
}
```
这种方式可以确保 Tailwind 在清除未使用的样式时包含 markstream-vue2 使用的所有工具类,从而获得更小的最终打包体积。
### 快速安装:所有功能
一次性启用所有功能:
```bash
pnpm add stream-markdown stream-monaco mermaid @terrastruct/d2 katex
# 或
npm install stream-markdown stream-monaco mermaid @terrastruct/d2 katex
```
### 功能详情
#### 代码语法高亮
需要安装 `stream-markdown`:
```bash
pnpm add stream-markdown
```
`stream-markdown` 已内置 `MarkdownCodeBlockNode` 所需的 Shiki runtime。若要在 `MarkdownRender` 中使用 Shiki,请覆盖 `code_block` 渲染器(或直接使用 `MarkdownCodeBlockNode`)。
```js
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
setCustomComponents({ code_block: MarkdownCodeBlockNode })
```
#### Monaco 编辑器
完整的代码块功能(复制按钮、字体大小控制、展开/折叠):
```bash
pnpm add stream-monaco
```
如果不安装 `stream-monaco`,代码块仍会渲染,但交互式按钮可能无法工作。
#### Mermaid 图表
渲染 Mermaid 图表:
```bash
pnpm add mermaid
```
#### D2 图表
渲染 D2 图表:
```bash
pnpm add @terrastruct/d2
```
#### KaTeX 数学公式渲染
数学公式渲染:
```bash
pnpm add katex
```
同时在应用入口文件中导入 KaTeX CSS:
```ts
import 'katex/dist/katex.min.css'
```
## 快速测试
导入并渲染一个简单的 markdown 字符串:
```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>
```
## 使用 Composition API(Vue 2.7+)
如果你使用的是 Vue 2.7 或已安装 `@vue/composition-api`:
```vue
<script>
import { defineComponent, ref } from '@vue/composition-api' // 或 Vue 2.7 的 'vue'
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 支持
markstream-vue2 包含 TypeScript 类型定义。对于 Vue 2.6.x,你可能需要配置 `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["@vue/composition-api"]
}
}
```
对于 Vue 2.7+,类型会自动包含。
@@ -0,0 +1,471 @@
# Vue 2 快速开始
在你的 Vue 2 项目中开始使用 markstream-vue2。
## 基础设置
### 1. 安装
首先,安装包:
```bash
pnpm add markstream-vue2
```
### 2. 导入样式
在你的主入口文件(如 `main.js` 或 `main.ts`)中:
```js
import Vue from 'vue'
import App from './App.vue'
import 'markstream-vue2/index.css'
// 对于 Vue 2.6.x,还需要安装并配置 @vue/composition-api
// import VueCompositionAPI from '@vue/composition-api'
// Vue.use(VueCompositionAPI)
new Vue({
render: h => h(App)
}).$mount('#app')
```
### 3. 使用组件
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
name: 'App',
components: {
MarkdownRender
},
data() {
return {
markdown: `# Hello Vue 2!
这是 **markstream-vue2** - 适用于 Vue 2 的流式 Markdown 渲染器。
## 功能
- 代码语法高亮
- Mermaid 图表
- 数学公式
- 还有更多功能!
\`\`\`javascript
console.log('Hello from Vue 2!')
\`\`\`
`
}
}
}
</script>
<template>
<div id="app">
<MarkdownRender :content="markdown" />
</div>
</template>
```
## 使用 Vue 2.7+(Composition API)
Vue 2.7 内置了 Composition API 支持:
```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!
这使用了 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>
```
## 使用 Vue 2.6.x(@vue/composition-api)
对于 Vue 2.6.x,安装 `@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)
```
然后使用方式与 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!
这使用了 @vue/composition-api。
`)
return { markdown }
}
})
</script>
<template>
<div id="app">
<MarkdownRender :content="markdown" />
</div>
</template>
```
## 启用可选功能
### 代码语法高亮
安装依赖:
```bash
pnpm add stream-markdown
```
```vue
<script>
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue2'
// 在 MarkdownRender 中使用 Shiki 代码块
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 图表
安装 mermaid:
```bash
pnpm add mermaid
```
导入样式。默认 loader 已经启用,只有在你手动关闭或需要自定义 loader 时才需要调用 `enableMermaid()`:
```js
import { enableMermaid } from 'markstream-vue2'
// main.js
import 'markstream-vue2/index.css'
// 可选:重新启用或覆盖 loader
enableMermaid()
```
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
components: { MarkdownRender },
data() {
return {
markdown: `#### Mermaid 图表
\`\`\`mermaid
graph TD
A[开始] --> B{能用吗?}
B -->|是| C[太好了!]
B -->|否| D[继续尝试]
\`\`\``
}
}
}
</script>
<template>
<MarkdownRender :content="markdown" />
</template>
```
### D2 图表
安装 D2:
```bash
pnpm add @terrastruct/d2
```
导入样式。默认 loader 已经启用,只有在你手动关闭或需要自定义 loader 时才需要调用 `enableD2()`:
```js
import { enableD2 } from 'markstream-vue2'
// main.js
import 'markstream-vue2/index.css'
// 可选:重新启用或覆盖 loader
enableD2()
```
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
components: { MarkdownRender },
data() {
return {
markdown: `#### D2 图表
\`\`\`d2
direction: right
Client -> API: request
API -> DB: query
DB -> API: rows
API -> Client: response
\`\`\``
}
}
}
</script>
<template>
<MarkdownRender :content="markdown" />
</template>
```
### 数学公式(KaTeX)
安装 katex:
```bash
pnpm add katex
```
导入样式。默认 loader 已经启用,只有在你手动关闭或需要自定义 loader 时才需要调用 `enableKatex()`:
```js
import { enableKatex } from 'markstream-vue2'
// main.js
import 'markstream-vue2/index.css'
import 'katex/dist/katex.min.css'
// 可选:重新启用或覆盖 loader
enableKatex()
```
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
components: { MarkdownRender },
data() {
return {
markdown: `#### 数学示例
行内数学公式:$E = mc^2$
块级数学公式:
$$
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
$$`
}
}
}
</script>
<template>
<MarkdownRender :content="markdown" />
</template>
```
## 自定义组件
你可以使用 `setCustomComponents` 自定义特定节点的渲染方式:
```vue
<script>
import MarkdownRender, { setCustomComponents } from 'markstream-vue2'
// 创建自定义标题组件
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: `# 自定义标题
此标题使用自定义组件渲染。
`
}
},
mounted() {
// 注册自定义组件
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>
```
## 流式内容
markstream-vue2 支持流式 markdown 内容,适用于 AI 生成的内容:
```vue
<script>
import MarkdownRender from 'markstream-vue2'
export default {
components: { MarkdownRender },
data() {
return {
markdown: '',
fullText: `# 流式传输演示
此内容正在**逐步**流式传输。
## 功能
1. 渐进式渲染
2. 无布局偏移
3. 流畅动画
\`\`\`javascript
const streaming = true
console.log('流式传输已启用:', 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">
开始流式传输
</button>
<MarkdownRender :content="markdown" />
</div>
</template>
```
## VitePress 集成(Vue 2)
对于使用 Vue 2 的 VitePress,你可以在自定义主题中使用 markstream-vue2:
```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 }) {
// 全局注册组件
app.component('MarkdownRender', MarkdownRender)
// 设置自定义组件(如果需要)
setCustomComponents('vitepress', {
// 你的自定义组件
})
}
}
```
## 下一步
- 查看 [Vue 2 组件文档](/zh/guide/vue2-components) 了解所有可用组件
- 查看 [API 参考](/zh/guide/api) 获取详细 API 文档
- 查看 [示例](/zh/guide/examples) 获取更多使用示例
@@ -0,0 +1,7 @@
# 为什么使用 markstream-vue
- 渐进式 Mermaid:图表可逐步渲染,用户更早看到结果。
- 流式 diff 代码块:随着数据到达即时显示 diff,反馈迅速。
- 针对大文档进行了优化:减少 DOM 更新与内存占用,适合实时/大型文档场景。
此库特别适用于实时 AI 驱动或大文档场景,在这些场景下静态 Markdown 管道可能会造成延迟或 UX 崩溃。
@@ -0,0 +1,123 @@
---
layout: home
description: 面向 Vue、React、Angular 和 Nuxt 的流式 Markdown 渲染器,提供安装、VitePress、AI 聊天、组件覆盖与迁移的任务式文档入口。
hero:
name: markstream-vue
text: 面向流式输出的 Markdown 渲染器
tagline: Vue 3 • Vue 2 • React • Angular • Nuxt
actions:
- theme: brand
text: 按场景安装
link: /zh/guide/installation
- theme: alt
text: 按任务找入口
link: /zh/guide/
- theme: alt
text: 接入与迁移
link: /zh/guide/ai-workflows
features:
- title: 先装对依赖
details: 按文档站、聊天应用、Monaco 代码块、图表型内容来选择最小 peer 依赖组合。
link: /zh/guide/installation
- title: 选对接入方式
details: 一次性渲染、VitePress 文档站、SSE 或 token 流式输出,推荐的接法并不一样。
link: /zh/guide/usage
- title: 先找对 API 层级
details: 查解析器钩子和作用域时看 API 参考;已经知道组件名时看渲染器与节点组件。
link: /zh/guide/api
- title: 覆盖内置组件
details: 用带作用域的方式替换 `image`、`code_block`、`mermaid`、`link` 等节点渲染器。
link: /zh/guide/component-overrides
- title: 扩展自定义标签
details: 把 `thinking` 这类 HTML-like 标签直接变成自定义节点,而不是自己重写解析器。
link: /zh/guide/custom-components
- title: 更平滑地迁移
details: 从现有 Markdown 方案迁移时,先看哪些能力可以直迁,哪些地方要换思路。
link: /zh/guide/react-markdown-migration
- title: 更好地和 AI 协作
details: 提供适合 Codex、Cursor、Claude Code 等工具的任务模板和 skill 清单。
link: /zh/guide/ai-workflows
---
适用于 Vue 3 的流式 Markdown 渲染器,支持 Mermaid 图表、流式代码块对比和高性能大文档处理。
<SupportQRCodes
title="支持 markstream-vue"
description="如果 markstream-vue 对你的工作有帮助,欢迎通过支付宝或微信支持项目的持续维护。"
note="感谢你的支持,这会帮助文档、演示和包维护持续迭代。"
alipay-label="支付宝"
wechat-label="微信收款"
/>
## 先走最短路径
| 如果你现在想解决的是... | 先看这里 | 然后看 |
| --- | --- | --- |
| 把第一段渲染跑起来 | [安装](/zh/guide/installation) | [快速开始](/zh/guide/quick-start) |
| 接到文档站或 VitePress 主题里 | [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration) | [自定义标签与高级组件](/zh/guide/custom-components) |
| 接入流式输出 / SSE | [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) | [性能](/zh/guide/performance) |
| 接入坏了但还不知道是哪一层出问题 | [按症状排查](/zh/guide/troubleshooting-path) | [排查问题](/zh/guide/troubleshooting) |
| 替换一个内置节点渲染器 | [覆盖内置组件](/zh/guide/component-overrides) | [渲染器与节点组件](/zh/guide/components) |
| 支持 `thinking` 这类可信标签 | [自定义标签与高级组件](/zh/guide/custom-components) | [API 参考](/zh/guide/api) |
| 做 parser / AST 级改造 | [API 参考](/zh/guide/api) | [解析器 API](/zh/guide/parser-api) |
| 借助 AI 做接入、迁移或排障 | [AI / Skills 工作流](/zh/guide/ai-workflows) | [从 react-markdown 迁移](/zh/guide/react-markdown-migration) |
## 按你的角色选入口
### 我是第一次接触 markstream
- 先看 [指南首页](/zh/guide/),它是按任务组织的总入口。
- 如果你已经知道框架,只想装对依赖,直接看 [安装](/zh/guide/installation)。
- 如果你想先看到最小 Vue 3 示例,直接看 [快速开始](/zh/guide/quick-start)。
### 我是在现有项目里接入
- 用 [使用与流式渲染](/zh/guide/usage) 决定该走 `content` 还是 `nodes`。
- 如果本质上是在做文档站、内容站或 VitePress 主题,优先走 [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration)。
- 如果页面会持续更新,优先走 [AI 聊天与流式输出](/zh/guide/ai-chat-streaming) 这条完整路径。
- 如果你还不知道到底是 CSS、peers、SSR 还是自定义标签的问题,先走 [按症状排查](/zh/guide/troubleshooting-path)。
- 如果安装能跑但页面效果不对,先看 [故障排除](/zh/guide/troubleshooting)。
- 如果是 Nuxt / SSR,优先看 [Nuxt SSR](/zh/nuxt-ssr)。
### 我是在做业务定制
- 已经知道目标组件时,看 [渲染器与节点组件](/zh/guide/components)。
- 需要安全替换内置节点时,看 [覆盖内置组件](/zh/guide/component-overrides)。
- 需要 parser hooks、AST 改造、作用域覆盖时,看 [API 参考](/zh/guide/api) 和 [解析器 API](/zh/guide/parser-api)。
### 我想借助 AI 提高效率
- 看 [AI / Skills 工作流](/zh/guide/ai-workflows),里面有 skills、prompts 和推荐接入顺序。
- 如果你用的是可读仓库的助手,再配合 [AI / LLM 上下文](/llms.zh-CN)。
## 选择你的框架
::: tip 框架支持
各框架共享同一套核心渲染思路,但入口页会因为 SSR、迁移路径和运行时差异而不同。
:::
| 框架 | 最适合先看的页面 | 适合什么情况 | 演示 |
| --- | --- | --- | --- |
| Vue 3 (`markstream-vue`) | [指南首页](/zh/guide/) | 你要走主线能力最完整的接入路径 | [在线演示](https://markstream-vue.simonhe.me/) |
| VitePress 文档站 | [文档站与 VitePress 集成](/zh/guide/vitepress-docs-integration) | 你要把渲染器嵌进文档页、内容站或自定义主题 | [在线演示](https://markstream-vue.simonhe.me/) |
| Nuxt | [Nuxt SSR](/zh/nuxt-ssr) | 你需要处理 client-only 边界、SSR 和 worker | [在线演示](https://markstream-nuxt.pages.dev/) |
| Vue 2 (`markstream-vue2`) | [Vue 2 快速开始](/zh/guide/vue2-quick-start) | 你还在 Vue 2.6 / 2.7 环境 | [在线演示](https://markstream-vue2.pages.dev/) |
| React (`markstream-react`) | [React 快速开始](/zh/guide/react-quick-start) | 你是 React 用户,或正从 `react-markdown` 迁移 | [在线演示](https://markstream-react.pages.dev/) |
| Angular (`markstream-angular`) | [Angular 快速开始](/zh/guide/angular-quick-start) | 你使用 standalone Angular 组件 | [在线演示](https://markstream-angular.pages.dev/) |
## 常用入口
- [API 参考](/zh/guide/api):解析器工具、作用域覆盖和渲染流程入口
- [渲染器与节点组件](/zh/guide/components):导出的渲染器和节点组件参考
- [按症状排查](/zh/guide/troubleshooting-path):先做第一轮定位,再进入对应深度页面
- [故障排除](/zh/guide/troubleshooting):CSS/reset 顺序、依赖项和常见问题
- [功能特性](/zh/guide/features):流式渲染、Mermaid、Monaco、KaTeX 等能力总览
- [站内搜索](/zh/guide/search):直接搜索页面、组件名和关键字
- [AI / LLM 上下文](/llms.zh-CN):给可读仓库的助手提供项目地图
## 文档是怎么组织的
- `开始使用` 先解决接入问题:安装、快速开始、流式接法、Props。
- `自定义` 负责解决业务扩展:覆盖组件、自定义标签、解析器钩子、样式与排障。
- `功能专题` 现在默认折叠,主要放代码块、Mermaid、KaTeX、D2、Monaco 等深水区内容。
- `框架与迁移` 适合你已经明确框架或迁移目标之后再进入。
@@ -0,0 +1,229 @@
# Worker Cache 性能分析总结
## 快速回答
**Worker 的 cache 非常重要且有意义!**
## 核心价值
### 1. **性能提升巨大** 🚀
- Cache 命中时间: **~0.01ms**
- Worker 渲染时间: **5-50ms**
- 提升倍数: **500-5000x**
### 2. **避免跨线程通信开销** 📡
```
无 Cache: 主线程 → Worker → 渲染 → 主线程 (每次 1-2ms 开销)
有 Cache: 内存读取 → 立即返回 (<0.01ms)
```
### 3. **内存占用极小** 💾
- 单条缓存: ~180 bytes
- 200 条缓存: ~36 KB
- 结论: **可以忽略不计**
## 实际测量数据
### 测试场景:真实文档(50 个公式)
| 方案 | 总耗时 | 主线程阻塞 | 缓存命中率 |
|------|--------|-----------|-----------|
| 无优化(直接渲染) | 250ms | 250ms | 0% |
| 仅 Worker(无缓存) | 265ms | 0ms | 0% |
| **Worker + Cache** | **75ms** | **0ms** | **70%** |
**性能提升: 3.3x** ✅
**用户体验: 完全流畅** ✅
### 公式复杂度对比
| 类型 | 示例 | 首次渲染 | 缓存命中 | 提升 |
|------|------|----------|----------|------|
| 简单 | `x = y` | 2ms | 0.005ms | 400x |
| 中等 | `\sum_{i=1}^{n}` | 10ms | 0.008ms | 1250x |
| 复杂 | `\int f(x) dx` | 30ms | 0.01ms | 3000x |
## 为什么 Cache 如此有效?
### 真实文档的特点:
1. **高重复率**:变量 `x`, `y`, `z` 经常出现
2. **模板公式**:`\sum`, `\int` 等常见结构
3. **增量渲染**:滚动、组件重新挂载会重复请求
### 实际缓存命中率统计:
```
数学文档: 70-85% 命中率
技术文档: 60-75% 命中率
教程文档: 75-90% 命中率(大量重复示例)
```
## 性能监控方法
### 方法 1: 运行基准测试
```bash
pnpm test test/benchmark/katex-worker-vs-direct.test.ts
```
### 方法 2: 浏览器控制台
```javascript
// 启用监控
window.__katexPerfMonitor.enable()
// 使用应用...
// 查看报告
window.__katexPerfReport()
```
### 方法 3: Chrome DevTools
1. 打开 Performance 标签
2. 录制渲染过程
3. 查看 Main 线程活动
4. 检查是否有长任务(>50ms)
**判断标准:**
- ✅ 如果看到 Main 线程有长时间黄色块 → Worker 有价值
- ✅ 如果 Worker 线程有独立活动 → Cache 在工作
- ⚠️ 如果两者都很空闲 → 公式太简单
## 决策树
```mermaid
graph TD
A[需要渲染数学公式?] -->|是| B{公式复杂度?}
A -->|否| Z[不需要 Worker]
B -->|简单 <5ms| C{数量多吗?}
B -->|中等 5-20ms| D[✅ 使用 Worker + Cache]
B -->|复杂 >20ms| D
C -->|<5 个| E[可选 Worker]
C -->|>5 个| D
D --> F{有重复公式吗?}
F -->|是 >30%| G[✅✅ Cache 价值巨大!]
F -->|否| H[✅ Worker 仍防止阻塞]
```
## 最终建议
### ✅ **保留当前架构**
你的代码已经是最佳实践:
```typescript
// 1. 先检查 Cache(<0.01ms)
const cached = cache.get(cacheKey)
if (cached)
return cached
// 2. 使用 Worker 渲染(不阻塞主线程)
renderKaTeXInWorker(content)
.then((html) => {
// 3. 保存到 Cache(未来受益)
cache.set(cacheKey, html)
})
.catch(() => {
// 4. Fallback 到直接渲染(稳定性)
katex.renderToString(content)
})
```
### 优势总结
| 特性 | 价值 |
|------|------|
| 性能 | ✅✅✅ Cache 提升 500-5000x |
| 体验 | ✅✅✅ Worker 避免 UI 卡顿 |
| 内存 | ✅✅✅ 仅 36KB (200 条) |
| 稳定性 | ✅✅✅ Fallback 机制保底 |
| 维护性 | ✅✅ 代码清晰简洁 |
## 常见问题
### Q: Cache 会不会内存泄漏?
**A:** 不会。使用 LRU 策略,限制最多 200 条。
### Q: SSR 环境怎么办?
**A:** 自动 fallback 到直接渲染,无需特殊处理。
### Q: Worker 初始化失败怎么办?
**A:** 代码已处理,会 reject 并触发 fallback。
### Q: 需要手动管理 Cache 吗?
**A:** 不需要。自动管理,开发者零负担。
## 性能优化技巧(可选)
### 1. 预热常见公式
```typescript
const common = ['x', 'y', 'E=mc^2']
common.forEach(f => renderKaTeXInWorker(f))
```
### 2. 调整缓存大小
```typescript
// 如果有大量唯一公式
const CACHE_MAX = 500 // 默认 200
```
### 3. 使用 requestIdleCallback
```typescript
requestIdleCallback(() => {
// 在浏览器空闲时预渲染
renderKaTeXInWorker(formula)
})
```
## 结论
**Worker 的 Cache 不仅有意义,而且是性能关键!**
- ✅ 提升性能 3-10 倍
- ✅ 改善用户体验(无卡顿)
- ✅ 内存占用可忽略
- ✅ 代码维护性好
**不要删除 Cache!它是整个架构的核心价值所在。** 🎯
## 组件级回退策略与差异(Inline vs Block)
为保证在高并发/高压力场景下不出现“无限 Loading”,组件层实现了差异化回退策略:
- MathBlock(块级公式)
- 优先使用 Worker 渲染。
- Worker 初始化失败(code: WORKER_INIT_ERROR)时,回退到主线程同步渲染(katex.renderToString,displayMode: true)。
- 同步渲染成功后,会通过 setKaTeXCache 预热 Worker 客户端缓存(key 区分 displayMode),以便后续同一公式命中缓存、避免重复成本。
- MathInline(行内公式)
- 优先使用 Worker 渲染,带回压重试。
- 发生以下错误时会尝试主线程同步渲染(displayMode: false),避免长时间处于 Loading:
- WORKER_INIT_ERROR(Worker 初始化失败)
- WORKER_BUSY(达到并发上限,忙)
- WORKER_TIMEOUT(单次 Worker 渲染超时)
- 如果同步渲染不可用或失败,将停止 Loading 并回退展示原始文本(raw),确保用户不看到“永久转圈”。
- 默认不调用 setKaTeXCache 进行缓存预热(与块级不同);这样做是出于实现简洁和收益权衡。若需要最大化命中率,可在同步回退成功后调用缓存预热(可选)。
### 设计权衡
- 行内公式通常更轻量、数量更多且重复度不稳定,主线程同步一次的代价较小,因此在 Worker 繁忙/超时时进行同步回退能显著改善体验。
- 块级公式复杂度和复用度通常更高,缓存预热能带来更明显的收益,所以默认启用。
### 相关错误码
- WORKER_INIT_ERROR:未注入/未能初始化 Worker;需要回退到主线程。
- WORKER_BUSY(常量:WORKER_BUSY_CODE):达到并发上限;可等待或回退。
- WORKER_TIMEOUT(name/code:"WorkerTimeout"/"WORKER_TIMEOUT"):单次渲染超时;可回退。
### 可选增强(按需开启)
- 如需让行内公式也享受回退后的缓存命中,可在同步渲染成功后调用 setKaTeXCache(content, false, html)。
- 若极端场景中担心主线程压力,可仅对超时(WORKER_TIMEOUT)而不是忙(WORKER_BUSY)启用同步回退,或增加等待窗口后再回退。
@@ -0,0 +1,155 @@
# KaTeX Worker 性能(重定向)
该页面已迁移到中文指南目录:
- 新页面:`/zh/guide/katex-worker-performance-analysis`
如果你的链接仍指向 `/zh/katex-worker-performance-analysis`,页面将自动跳转。
<script>
if (typeof window !== 'undefined') {
window.location.replace('/zh/guide/katex-worker-performance-analysis')
}
</script>
<noscript>
页面已迁移:请访问 <a href="/zh/guide/katex-worker-performance-analysis">中文指南 – KaTeX Worker 性能</a>
</noscript>
# KaTeX Worker 性能分析指南(中文)
## 问题:Worker 是否比直接渲染更合适?
本文档帮助你判断在何种场景下使用 Web Worker 会带来性能优势。
## 快速结论
**是的,Worker + Cache 架构在大多数真实场景中有显著优势。**
关键原因:
1. Cache 能消除大部分重复渲染开销(缓存命中率通常 >70%)
2. Worker 能阻止主线程被阻塞,从而保证 UI 响应
3. 内存占用极小(举例:200 个公式大约 10–50KB)
## 简要对比(场景)
- 场景:单个简单公式
- 直接渲染:约 2–5ms
- Worker:约 3–7ms(含通信开销)
- 结论:Worker 略慢,但差异可以忽略
- 场景:单个复杂公式
- 直接渲染:约 20–50ms(阻塞主线程)
- Worker:约 22–52ms(不阻塞)
- 结论:Worker 明显改善用户体验
- 场景:重复公式多(带缓存)
- 直接渲染:5ms × 10 次 = 50ms
- Worker + Cache:5ms + 0.01ms × 9 次 ≈ 5.09ms
- 结论:Worker 快约 10 倍
- 真实文档(混合场景)
- 50 个公式,其中 35 个重复
- 无缓存:250ms(全部渲染)
- 有缓存:75ms(仅渲染 15 个)
- 缓存命中率:70%
- 性能提升:3.3×
## 如何运行基准测试
1. 安装依赖
```bash
pnpm install
```
2. 运行 KaTeX Worker 对比测试
```bash
pnpm test test/benchmark/katex-worker-vs-direct.test.ts
```
3. 查看更详细报告
```bash
pnpm test test/benchmark/katex-worker-vs-direct.test.ts -- --reporter=verbose
```
## 如何判断是否应该启用 Worker
当一次渲染的“唯一公式数”大于阈值 N 时,使用 Worker 更安全。经验公式:
N ≈ floor(B / (R × (1 - H)))
- B:主线程预算(ms),常用 50ms(卡顿阈值)或 16.7ms(单帧预算)
- R:单个“唯一公式”的平均渲染耗时(ms)
- H:缓存命中率(0..1)
仓库中提供了一个测量脚本:
```bash
node scripts/measure-katex-threshold.mjs
```
此外,`markstream-vue` 提供了辅助函数可推荐阈值:
```ts
import { recommendNForSamples, recommendWorkerThreshold } from 'markstream-vue/utils/katex-threshold'
```
## 浏览器中实时监控性能
可以在应用中启用性能监控:
```ts
import { enablePerfMonitoring, getPerfReport } from 'markstream-vue/utils/performance-monitor'
enablePerfMonitoring()
setTimeout(() => {
console.log(getPerfReport())
}, 30000)
```
或者在浏览器控制台运行:
```js
window.__katexPerfReport()
window.__katexPerfMonitor.exportMetrics()
```
## 使用 Chrome DevTools 分析
- Performance:录制渲染流程;查看主线程与 Worker 的时间
- Memory:检查 cache 的内存占用
- Performance Monitor:观察 CPU、JS heap、帧率等
## 决策表
推荐使用 Worker 的场景:
- 复杂公式
- 每页多个公式
- 大量重复公式(命中缓存)
- 需要流畅交互(滚动、动画)
- 移动端(CPU 弱)
适合直接渲染的场景:
- 仅简单公式(<5ms)
- SSR 或没有 Worker API 的环境
- 对包体积有严格限制
## 最佳实践
- 使用 Worker + Cache 作为默认策略,并保留主线程 fallback
- 对常见公式做预渲染或预热缓存
- 使用 requestIdleCallback 做非关键渲染
- 根据实际测量调整 `CACHE_MAX` 大小
## 测量示例与结论
- Worker+Cache 在多数混合文档场景下能将渲染时间减少到原来的约 1/3
- 内存开销很小,通常无需担心
- Worker 防止主线程阻塞,提高用户体验
## 参考
- Chrome Performance 文档:https://developer.chrome.com/docs/devtools/performance/
- Web Worker 性能说明:https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#performance_considerations
- KaTeX 性能提示:https://katex.org/docs/performance.html
@@ -0,0 +1,108 @@
# Monorepo 迁移(重定向)
该页面已迁移到中文指南目录:
- 新页面:`/zh/guide/monorepo-migration`
如果你的链接仍指向 `/zh/monorepo-migration`,页面将自动跳转。
<script>
if (typeof window !== 'undefined') {
window.location.replace('/zh/guide/monorepo-migration')
}
</script>
<noscript>
页面已迁移:请访问 <a href="/zh/guide/monorepo-migration">中文指南 – Monorepo 迁移</a>
</noscript>
# Monorepo 迁移指南
## 概览
为了解耦解析器与渲染器,项目将解析器抽离为独立包 `stream-markdown-parser`,以便跨框架复用和更灵活的打包策略。
## 包结构
```
markstream-vue/
├── packages/
│ └── markdown-parser/ # stream-markdown-parser
│ ├── src/
│ │ ├── index.ts # 入口
│ │ ├── config.ts # KaTeX 配置等
│ │ ├── types.ts
│ │ ├── parser/
│ │ └── plugins/
├── src/ # 主 Vue 包
└── playground/
```
## 变更概览
### 1. 新增包:`stream-markdown-parser`
- 纯 JS/TS,无 Vue 依赖
- 框架无关,可用于任意 JavaScript 项目
- 单个入口导出常用函数:`getMarkdown`、`parseMarkdownToStructure` 等
### 2. 对主包的影响
`markstream-vue` 仍作为主要交付包,但现在依赖 `stream-markdown-parser`:
```json
{
"dependencies": {
"stream-markdown-parser": "workspace:*"
}
}
```
### 3. 导入变更
对内部开发者:
- 旧:
```ts
import { getMarkdown } from './utils/markdown'
```
- 新:
```ts
import { getMarkdown } from 'stream-markdown-parser'
```
对外部用户:API 保持一致——更改主要是内部实现与包结构。
## 构建与测试
- 构建解析器包:
```bash
cd packages/markdown-parser
pnpm build
```
- 构建主包:
```bash
cd ../..
pnpm build
```
- 运行测试:
```bash
pnpm test
```
## 好处
- 更好的可复用性与更小的 bundle
- 清晰的边界与独立的版本策略
- 更便于单元测试与 CI 的维护
## 迁移建议
- 在代码中把 `import` 指向新的 `stream-markdown-parser`。
- 为 parser 提供单独的测试文件与基线,以便快速发现破坏性更改。
- 将 parser 的发行流程独立于主包。
@@ -0,0 +1,150 @@
description: 在 Nuxt 3 SSR 中使用 markstream-vue,获得服务端 HTML、稳定 fallback,以及重节点的客户端增强。
---
# Nuxt 3 SSR 用法示例
> For the English version, see [nuxt-ssr.md](../nuxt-ssr.md).
`markstream-vue` 现在可以直接在 Nuxt SSR 中产出服务端 HTML。为了“能跑”而把整个 `<MarkdownRender>` 包进 `<ClientOnly>`,已经不是默认推荐做法。
当前的 SSR 模型是:
- 普通 markdown、HTML、链接、表格、脚注、图片会直接在服务端渲染。
- 代码块会先输出稳定的服务端 `<pre><code>` fallback,再在客户端增强成 Monaco。
- 数学公式在提供同步 KaTeX loader 时,可以直接在服务端输出真正的 KaTeX HTML。
- Mermaid、D2、Infographic 会先输出可读的 SSR fallback,再在客户端增强。
- scoped 自定义组件覆盖、自定义 node type、以及 `customHtmlTags` 也都能参与服务端渲染。
## 最小 SSR 页面
```vue
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { ref } from 'vue'
const markdown = ref(`
# 来自 Nuxt SSR 的问候
行内公式:$E = mc^2$
\`\`\`ts
export const greet = (name: string) => \`hello \${name}\`
\`\`\`
\`\`\`mermaid
graph TD
SSR --> Hydration
\`\`\`
`.trim())
</script>
<template>
<MarkdownRender :content="markdown" :final="true" />
</template>
```
这样首包就已经有服务端 HTML;重节点仍然会在 hydration 后增强,但不会先给你一个空壳。
## 服务端渲染数学公式
如果你希望 SSR 响应里直接带 KaTeX HTML,可以在 Nuxt 应用里注册同步 loader:
```ts
import katex from 'katex'
import { enableKatex } from 'markstream-vue'
enableKatex(() => katex)
```
如果没有这个 loader,数学公式也仍然是 SSR-safe,只是会稳定回退成可读原文。
## 预解析 nodes 与自定义组件
除了直接传 `content`,你也可以在 SSR 中传 `nodes`,或者配合 scoped custom components / trusted custom tags 一起使用。
```ts
import { setCustomComponents } from 'markstream-vue'
import { defineComponent, h } from 'vue'
setCustomComponents('docs-ssr', {
thinking: defineComponent({
props: {
node: { type: Object, required: true },
},
setup(props) {
return () => h('aside', { 'data-ssr-thinking': '1' }, String((props.node as any).content ?? ''))
},
}),
})
```
```vue
<template>
<MarkdownRender
custom-id="docs-ssr"
content="<thinking>服务端自定义标签</thinking>"
:custom-html-tags="['thinking']"
:final="true"
/>
</template>
```
如果你的服务端已经拿到了 AST,优先用 `nodes + final`,这样可以避免浏览器端重复解析,SSR 输出也更可控。
## 什么时候还需要 `<ClientOnly>`
通常不是给渲染器本身用的。
只有当你自己的页面逻辑本身依赖浏览器 API,或者你明确想让一整块区域跳过 SSR 时,才需要 `<ClientOnly>`。
## Nuxt playground
仓库里已经提供了完整的 Nuxt playground,路径是 `playground-nuxt/`:
```bash
pnpm install
pnpm play:nuxt
```
专门的 SSR 验证路由是:
- `/ssr-lab`
这个页面里有固定矩阵,用来覆盖:
- 基础 markdown + HTML 的 SSR
- 重节点的 SSR fallback
- hydration 后的增强路径
- 显式禁用增强时的稳定 fallback
- 自定义标签与 scoped override 的验证
## SSR 验收命令
Nuxt SSR 的验收 e2e 命令是:
```bash
pnpm test:e2e:nuxt-ssr
```
它会同时验证:
- `nuxt dev`
- `nuxt build && nuxt preview`
每种模式都会先检查 `/ssr-lab` 的原始 HTTP HTML,再打开浏览器验证 hydration、增强、reload 和路由往返后的稳定性。
## 库级 SSR 回归覆盖
仓库里还有一层直接针对渲染器的 `renderToString` 回归测试:
```bash
pnpm test --run test/ssr-render-to-string.test.ts test/ssr-import.test.ts
```
这层测试显式覆盖了:
- 轻节点 SSR 组件矩阵,例如 heading、paragraph、行内格式、link、list、table、footnote、admonition、`vmr_container`
- 独立的 `MarkdownCodeBlockNode` SSR shell
- 通过 `setCustomComponents` 做 scoped 内置覆盖
- 直接自定义 node type 的 SSR
- 通过 `customHtmlTags` 接入的可信自定义标签 SSR