feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,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 = ''
|
||||
</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 = ''
|
||||
</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
|
||||
```
|
||||
````
|
||||
|
||||

|
||||
|
||||
## 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]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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: ''
|
||||
}
|
||||
|
||||
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 历史图:
|
||||
|
||||
[](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
|
||||
Reference in New Issue
Block a user