# MD Editor 产品组件

直接接入使用 QuireKit Prose 样式的 Markdown 文档编辑器。

## 先体验完整文档

[打开 MD Editor 独立示例](/md-editor)。同一份文档可以编辑、查看源码、只读浏览，
也可以通过 `MdProse` 查看静态阅读快照。示例包含嵌套提示块、折叠块、任务、列表、
引用、代码、表格、公式、流程图、思维导图、定义列表和脚注。模式切换保留编辑实例、选区与撤销历史。

`@quirekit/md-editor` 组合 MSFE 能力与 QuireKit 公共组件。源码、事务与解析仍属于
MSFE；正文外观由 Prose 定义。需要自己组合整个界面时，继续使用 [Vue 接入](/guide/vue)。

## 先选保存模型，再选编辑器

| 需求 | 选择与边界 |
| --- | --- |
| README、技术文章、Git 审阅、跨工具 Markdown | MD Editor 以源码为权威；文档和源码视图共享文本与历史 |
| 富文档、可视块、更丰富的表格和格式 | Block Editor 使用 PM 结构文档与产品界面；按当前已交付能力接入，导出 Markdown 是有损风险的转换 |
| 自定义 schema、节点视图、已有 Tiptap 生态 | 原生 Tiptap 提供框架与扩展，宿主组合 UI 和服务；结构能力由 schema 与扩展决定 |

GFM 表格支持粗体、斜体、删除线、链接和行内代码；不支持嵌套表格、列表、代码块、多个段落、
合并单元格和任意列宽。多种行内样式可以嵌套，但标记前后的标点与文字决定实际解析。
行内代码内容按字面解释。不要把当前尚未实现的局部格式拆分，混同为 Markdown 禁止样式叠加。

可视格式命令共用 [语法保真规划](/api/commands#语义格式命令)：保留既有样式并确认目标样式成立，
否则拒绝并保留原文；不会偷偷添加空格。单元格内允许行内格式，禁用标题、列表和块插入。
源码输入保持自由，不自动修复未完成或自定义语法。未知结构仍走源码回退。
扩展语法需要对应阅读器；仅保存 `.md` 不会自动包含评论、协作等外部状态。

完整对照与可编辑例子见 [固定默认文档](/md-editor)。规范依据：
[GFM 表格](https://github.github.com/gfm/#tables-extension-)、
[CommonMark 强调](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis)、
[Tiptap schema](https://tiptap.dev/docs/editor/core-concepts/schema)。

## 四种视图

桌面的代码语言使用紧凑下拉列表，可通过方向键和标签前缀选择；文字样式使用按钮旁的
选择面板。窄屏代码语言保留可搜索的底部面板，文字样式整合到底部文字格式任务。
未知的现有语言仍会列出，选择“纯文本”会清除围栏的语言标记。
代码语言选择立即写回，可一次撤销；取消选择保留原文。文字格式按钮也立即应用，
关闭面板不会撤销已应用的设置。
示例页在窄屏使用同样的面板切换文档视图，回到编辑时保留选区与撤销历史。

窄于 768px 时，正文使用紧凑留白，底部工具栏提供撤销、重做、插入、文字样式、列表和
导入/导出。底栏使用公共 Toolbar 移动模式，不显示 Tooltip；当前表格动作只有放不下时才
收进“更多”，点击打开底部动作面板。插入目录先展示图片、链接、表格等常用内容，再展示
图表、结构和文字格式；搜索和参数表单提供输入提示。文字格式提供“全选、选中当前块”，后者选择光标所在的
最近完整 Markdown 块。空行与不完整围栏不提供块选择，选择动作不写源码或进入历史。
打开设置面板后，已聚焦过的正文保留静止光标或范围选区。
源文档替换、只读/locked 会清理或隐藏静止光标。移动 live 的键盘关闭或窗口失焦保留
被禁用的自绘端点；点击已有选区只切换操作菜单，不重新聚焦或唤起键盘。

选区菜单使用公共 Toolbar，提供复制、剪切、粘贴和全选，执行后自动收起。
粘贴读取系统剪贴板中的纯文本；浏览器不允许读取或使用局域网 HTTP 时，会打开粘贴面板，
可在“粘贴内容”输入框中调用系统粘贴，再点击“粘贴”替换原选区。取消或空内容不改动正文。
粘贴和剪切均可一次撤销；等待读取期间若选区或文档已改变，旧请求会取消。

窄屏触摸设备聚焦可编辑正文后显示公共 `SelectionHandles` 光标手柄：
拖动圆点移动光标；长按正文或圆点先保留插入点，实际拖动才扩大选区，不预选词语。松手后可拖动任一端点，
跨行或反向调整时保持端点身份。选区移动不增加文本撤销记录。移动 `live` 模式的正文
退出原生可编辑状态，由独立输入代理和共享 `@quirekit/ime` 接收键盘与中文组合输入，
光标和选区由产品绘制，避免系统菜单、光标和选区重叠。预编辑文字临时显示在正文中，
确认只提交一笔 CodeMirror 事务并可一次撤销；组合输入时光标继续显示，但手柄暂不可拖动。
源码、桌面、只读和独立嵌入编辑器保留各自的输入方式。文档、选区、命令和历史始终由 MSFE/CM 持有。
隐藏语法和零高度间隙只接受两侧的可见光标位置。点击或拖动到表格分隔线时，光标会
落到相邻单元格的文字插入点，空单元格仍可输入；边界处的中文预编辑保留在对应单元格内。
拖选靠近可用区域边缘时自动滚动，
松手立即停止。键盘缩小可用区域后，光标与圆点一起滚到工具栏上方；已可见的连续输入
不重复滚动，用户手动滚动也不会被拉回原位置。
工具栏消费当前可视视口，键盘遮挡时不重复加入底部安全区；示例编辑器 route 禁用 pinch，
原生输入至少使用 16px 字体以减少聚焦缩放。真实 iOS 的键盘附件条和候选窗仍需设备验证。

MD Editor 有四种视图，编辑和显示的都是同一份 Markdown 原文，中间不经过任何格式转换：

| 视图 | 设置 | 行为 |
| --- | --- | --- |
| 文档视图（默认） | `view="document"` | 点击后仍隐藏已识别的标记，直接编辑正文；表格保持单元格，代码保留工具栏，容器保持连续结构 |
| 实时预览 | `view="preview"` | 段落或列表项的标记一起显露；活动表格显示结构源码、所选单元格显示行内标记，容器首尾围栏对齐 |
| 源码视图 | `view="source"` | 查看和编辑全部原文 |
| 阅读视图 | `view="reading"` | 用组件渲染当前文档，不可编辑；编辑器保持挂载，切回后选区和历史不变 |

只读不是单独的视图，而是叠加在前三种视图上的 `readOnly`：只停止输入，链接、折叠和复制照常可用，切换视图时保持不变。
四种视图共用一份 Markdown、选区、历史和 Prose 样式。示例页列出全部四种视图和只读开关，宿主也可以只提供其中几种。

```vue
<MdEditor :markdown="initialMarkdown" view="document" />
<!-- 改为 preview、source 或 reading 即切换视图，更新该属性无需重新挂载。 -->
```

无框架实例只包含三种可编辑视图（阅读视图是 Vue 组件里的渲染），通过公开方法切换：

```ts
import { createMdEditor, getMdView, setMdView } from '@quirekit/md-editor'

const editor = createMdEditor({ parent, markdown: '# 标题', view: 'document' })
const result = setMdView(editor, 'preview')
if (!result.ok) console.error(result.reason)
console.log(getMdView(editor)) // 当前视图
setMdView(editor, 'source', true) // 第三个参数设置只读
```

切换不修改源码、不进入撤销栈；拖选或组合输入期间延迟应用，避免改变正在输入的 DOM。
已隐藏的标记参与原生原子导航，列表回车继续列表，行首退格可以移除列表标记。
未知或不完整的语法仍保留可编辑源码；自定义投影应遵循 [源码显露合同](/api/rendering#源码显露)。
这不意味着任意 Markdown 插件自动拥有可编辑的文档 UI；复杂节点仍需声明投影、命令和回退。

实时预览按语义范围显露：跨行段落一起显示粗体、斜体、链接和行内代码的原始拼写；
任务项的列表前缀与 `[x]` / `[ ]` 同步显示，同级其他项保持阅读呈现。嵌套列表分别判断
各层列表项；容器里的段落独立判断，编辑一段不会显露其他段落的行内标记。
表格进入源码呈现时显示所有竖线和分隔行，但只显露所选单元格的行内标记。
输入法预编辑沿用这些范围，前后无关的标题、表格、代码、公式和图表继续保留呈现。

### 回车与块后输入

光标所在块会显示浅灰圆角背景，帮助识别当前段落、列表或引用的边界。容器内的正文按内部块
显示提示；这个提示不改源码、不增加间距，也不拦截鼠标。源码视图、只读和 阅读视图不显示它。

两种编辑体验都保留新建空段落的高度。被压缩的块间结构分隔行不接受正文光标，导航会落到
相邻真实内容；源码视图可以在这些行输入，切回后按新语法正常显示正文。
正文、ATX 标题及嵌套
容器里的段落按 `Enter` 会插入 Markdown 段落分隔，中间输入会拆分段落；空文档、连续回车和退出空列表后
都可以直接继续输入。块间纯排版分隔仍由 Prose 控制，编辑动作不重新序列化整篇文档。

| 位置或按键 | 行为 |
| --- | --- |
| 正文、ATX 标题里的 `Enter` | 从光标处分段；末尾开始新段落，标题后恢复正文 |
| 列表、引用里的 `Enter` | 续写当前标记；空列表项或空引用行直接退出一层，退出到正文时保留段落分隔 |
| 代码正文里的 `Enter` | 在当前代码块内换行 |
| 文档视图的表格单元格里 `Enter` | 在当前行后新增一行，保留列数与对齐；`Tab` / `Shift+Tab` 继续切换单元格 |
| `⌘+Enter` / `Ctrl+Enter` | 在当前代码块、容器、表格、列表或引用后开始段落；嵌套块保留外层引用和列表缩进 |
| 正文里的 `Shift+Enter` | 插入两个尾随空格加换行的 Markdown 硬换行；保留外层列表缩进与引用前缀，不插入 `<br>` |

这些换段规则由 `document` 与 `markdown` 共用；纯源码视图保持原生源码输入。
引用末尾第一次回车立即显示引用内的可编辑空行，再按一次退出当前引用层级；嵌套引用逐层退出。
代码正文保留字面换行，实时预览中的表格保留源码操作；Setext 标题末尾在下划线之后换段。
换段可以单独撤销，只读与输入法组合状态继续由同一编辑实例管理。

## 光标与编辑边界

文档模式下，正文光标只能进入可编辑内容。功能插件声明完整结构范围和内部正文范围，
公共运行时据此处理方向键、Home/End、点击、拖选、删除和跨边界替换。所有范围使用
Markdown 源码偏移；隐藏标记和压缩空行不会产生第二套行号或文档。

默认行内代码保留样式外侧与内侧两个插入位置，加粗、斜体、高亮等直接进入内容。
代码围栏和语言信息受保护，代码正文可编辑；部分跨块删除保留外壳，完整选中块可以
整体删除。属性面板、图表等已投影的块组件按整体对象处理，边缘删除先选中整体。
正常回车、显式换行与退出块继续使用 `addBlockInput()`，见 [块级回车 API](/api/commands#块级回车)。

### 覆盖产品默认规则

`editingBoundaries` 按解析树节点名称覆盖规则。下面把行内代码改为直接进入内容；
将 `navigation` 改为 `split` 可以恢复默认的内外两侧停留。宿主须先准备 `#editor` 容器。

```ts
import type { EditingBoundaryRule } from '@quirekit/msfe-core'
import { createMdEditor } from '@quirekit/md-editor'

const inlineCodeRule: EditingBoundaryRule = {
  nodes: ['InlineCode'],
  /** @param context - 当前已确认的语法。@returns 成对标记及可编辑正文，未闭合时返回 null。 */
  resolve({ node }) {
    const [opening, closing] = node.getChildren('CodeMark')
    if (!opening || !closing) return null
    return {
      from: node.from, to: node.to,
      contentFrom: opening.to, contentTo: closing.from,
      kind: 'style', navigation: 'content', deletion: 'unwrap',
    }
  },
}

const editor = createMdEditor({
  parent: document.getElementById('editor')!,
  markdown: '正文 `code` 正文',
  view: 'document',
  editingBoundaries: [inlineCodeRule],
})
```

`nodes` 是解析树名称，`from/to` 覆盖反引号及文字，`contentFrom/contentTo` 只覆盖代码
内容；`deletion: 'unwrap'` 使边缘删除成对移除标记。规则查询不提交事务，也不改变
光标像素坐标。宿主离开时调用 `editor.destroy()` 释放实例。

Vue 沿用同一个 `inlineCodeRule`，通过组件属性传入：

```vue
<MdEditor :markdown="initialMarkdown" :editing-boundaries="[inlineCodeRule]" />
```

规则在创建/挂载时读取；改变规则集合需要重建实例，并由宿主保留需要的文档。
`editingBoundaries: []` 表示没有宿主覆盖，内置规则继续生效。同一节点采用最高优先级
的第一条规则；该规则返回 `null` 表示退出该节点的语法边界处理，不再回退到默认规则。
已投影组件的整体对象保护独立保留。

### 在功能插件中声明

属于功能自身的规则放在 `addEditingBoundaries()`。以已有的行内代码语法为例，沿用
上面的规则声明一个扩展；宿主可以通过 `extensions: [InlineCodeBoundaries]` 安装它：

```ts
import { Extension } from '@quirekit/msfe-core'

const InlineCodeBoundaries = Extension.create({
  name: 'inline-code-boundaries',
  priority: 1000,
  /** @returns 导航、选区和编辑共用的语法范围。 */
  addEditingBoundaries() { return [inlineCodeRule] },
})
```

插件接入时使用此扩展承担规则，产品的 `editingBoundaries` 留给业务宿主覆盖。
已有插件可在自身 hook 中返回规则，或通过 `.extend()` 与父 hook 组合；同一节点的覆盖
规则应排在父规则前。该 hook 只描述边界，新增语法仍需由插件注册解析和投影。

`when` 默认是 `hidden`，只在整体隐藏语法的呈现中启用；`prose` 可在 实时预览
呈现中查询，规则应使用 `isSourceRevealed(state, node)` 判断具体结构是否已经显露。
源码视图关闭边界约束；在源码分隔行输入文字后，切回文档模式会按新语法把它作为正文。

当前 `navigation: 'native'` 会退出整条语法边界规则；若希望保留导航约束、将正文修改
交给原生处理，应使用 `deletion: 'native'`。旧 `inlineBoundaries` 通过同一个运行时兼容，
只替换原行内规则列表。六类边界、完整字段与高级 CM 接入见 [编辑边界 API](/api/commands#编辑边界)。

## 标题折叠

有正文的标题在左侧正文区域外提供折叠箭头，不挤占标题宽度。展开时悬停或键盘聚焦才显示，
收起后一直显示；触摸设备保留入口。点击后保留标题，收起到下一个同级或更高级标题之前的内容；
再次展开会保留子标题各自的折叠选择。支持 ATX 和 Setext 标题，容器、引用及列表项内的标题
以所在容器为边界。

文档视图、实时预览、只读与源码视图共用同一份折叠状态，折叠不改 Markdown，也不占用
文本撤销记录。查找定位到隐藏正文时会自动展开；改变标题层级导致旧范围失效时也会展开。
折叠选择不写入文件，刷新或替换文档后重置；Prose 静态阅读继续展示完整内容。

该能力归 `@quirekit/msfe-heading`，基础 MSFE 接入可通过 `Heading.configure({ folding: false })`
关闭标题控件及章节校验，原有 CodeMirror 源码折叠仍可使用。宿主可用 `controls` 和 `toggleLabel`
接入自己的组件与文案；MD Editor 默认使用公共 `QuireIconButton` 和统一图标。

## 块手柄与菜单

将鼠标移到块上，左侧会显示灰底描边的块类型与拖柄按钮，位于标题折叠箭头的外侧。点击打开菜单，或在正文中按
`⌘+Alt+B` / `Ctrl+Alt+B`。菜单使用公共 QuireKit 组件，可转换正文、标题和列表，
在上下插入内容，以及复制 Markdown、剪切、创建副本和删除。转换区只展示当前块可以执行的选项：
列表显示列表类型转换，代码、表格和公式等复杂块隐藏转换区，避免丢失结构。
窄屏触摸设备隐藏桌面悬停拖柄，插入入口保留在工具栏中；已经打开的块菜单继续保留锚点。

上下插入菜单按功能分组，在公共 `QuireScrollArea` 内滚动，包含当前安装的标题、Markdown 块、
公式、流程图、思维导图、白板、文本绘图，以及日期、表情和宿主提供的联系人。
图片、链接、公式和图表等先打开参数编辑器，确认后在原块前后一次插入；取消不会留下空块。
期间原块被改写、文档被替换或权限变为只读时，过时提交会被拒绝。

“流程图”“思维导图”和“白板”分别打开共享 Flow Editor、Mind Editor 和 Whiteboard Editor。
“文本绘图”打开共享源码绘图工作台，可选择 Mermaid、PlantUML 或 Graphviz / DOT。
Mermaid 的流程图、思维导图、时序图等在文本绘图里选择模板编写，保存为 Mermaid 代码块；
可拖拽编辑的流程图和思维导图保留各自的可编辑数据。
工具栏、空行加号和 `/` 菜单共用这些入口，图表分组放在段落列表之前。

默认示例文档直接包含项目交付流程、项目计划思维导图和灵感白板，保留完整可编辑数据及
从同一数据保存的 PNG 预览；双击即可继续编辑。单击会整体选中图形并浮出操作栏，
可编辑、预览、下载 PNG、复制、剪切或删除，白板还可重命名；选中后按 Delete 删除、Esc 取消。
预览会把文档里的图片和这几类图形按顺序放进同一图集，可左右切换。文本绘图部分还包含 PlantUML 时序图和
Graphviz / DOT 交付图，与公式一样单击整体选中、双击打开编辑；预览与下载 SVG 也在操作栏里。切换到 阅读视图可查看同一份内容。

光标进入或鼠标悬停可编辑空行时，左侧显示灰底描边的“＋”，点击直接打开同一插入目录。
选择内容后替换该空行并保留必要的段落分隔，一次撤销恢复原文；取消菜单或参数编辑不改变源码。
代码、公式、列表内部的空白不作为独立插入位置；已声明子流的容器可在其空行内插入。

空段落中输入 `/` 也可打开同一目录，继续输入中文名称、英文别名或拼音首字母搜索，
例如 `/表格`、`/table`、`/bg` 或 `/h2`。上下方向键选择、Enter 插入，Esc 关闭并保留输入的文字。
图片、公式等复用同一个参数编辑器，取消保留 `/查询`，确认后一次撤销可恢复它。
菜单保留正文输入焦点，支持中文输入；普通正文、URL、代码、引用和列表内部不触发。
此入口独立于块拖柄，默认开启，可通过 `:slash-menu="false"` 动态关闭。

拖动手柄时显示原块摘要与插入线，松手才移动源码；一次撤销恢复整个操作。列表、引用、代码、
表格、公式与容器整体移动，容器内部的直接子块可在同一容器中排序。当前不做跨容器移动或
列表内部逐项拖动；会破坏外层围栏的插入不可用。`Escape` 或失焦取消拖动，源码更新会关闭旧菜单。
源码视图、只读和 阅读视图不显示操作手柄。行内公式直接点击公式编辑，不再附带铅笔按钮。

Vue 组件默认开启，可在挂载时用 `:block-controls="false"` 关闭。自建界面可使用
`@quirekit/msfe-core` 的 `createSourceBlockControls`，让核心提供当前块、视口几何、拖动状态及
版本化命令目标；宿主负责用自己的组件展示。自定义嵌套语法通过 `sourceBlockScopes` 声明子流。
空行通过可选的 `insertionChange` 和 `openInsertionMenu` 回调提供 `SourceInsertionControls`；
`sourceBlankLineAt` 查询当前位置，`insertAtSourceBlankLine` 负责受保护的空行插入，不构造虚假块节点。
`rect` 是整块边界，`firstLine` 是首个可见原生文本行的矩形，可用于让控件对齐换行标题的首行；
不含原生文本行的块组件返回 `null`，宿主可退回块顶部定位。
见 [块控制协议](/api/rendering#块控制协议) 与 [源码块命令](/api/commands#源码块命令)。

每个 `MdEditor` 有独立的文档浮层，容纳块控件、块菜单、提及及工具栏插入面板。后代组件可从
`@quirekit/md-editor/vue` 调用 `useMdOverlayTarget()`，将返回的只读 ref 传给公共 Menu 的
`portalTarget`；子菜单自动继承同一目标。Menu 仍负责定位、视口避让与关闭，模态编辑器继续使用
公共 Dialog。该接口只提供挂载容器，不负责块命令或右键菜单的业务行为。

## 安装与宿主准备

当前包通过工作区或本地打包产物使用，尚未承诺公共 npm 稳定发布。

```sh
pnpm add '@quirekit/md-editor@workspace:*' '@quirekit/ui@workspace:*' '@quirekit/unocss-preset@workspace:*'
```

沿用应用的 QuireKit 主题、图标 Provider 和 UnoCSS / 独立样式入口。产品包没有第二套
颜色或正文 CSS。配置方法见 [UI 与主题](/customization/theme)。SSR 应用应在客户端挂载
`MdEditor`，例如在 Nuxt 的 `ClientOnly` 中使用。

产品打包时另有 `@quirekit/md-editor/styles.css`，只负责工具栏和文档区域的布局，需显式
引入。正文的字体、间距和块外观仍来自公共 Prose 样式。

编辑底层使用源码维护的 `@quirekit/cm-*` 与 `@quirekit/lezer-markdown`。相邻块间距的
高度测量、旧视图清理与语法复用观察随包构建交付，无需额外安装 CodeMirror/Lezer 补丁。
每个包的 `UPSTREAM.json` 记录上游版本、commit 与本地差异，原始 MIT 许可随包保留。
原生 CodeMirror 扩展也必须从同一组 `@quirekit/cm-*` 导入；依赖官方 `@codemirror/*`
的第三方扩展需适配后使用，避免在同一编辑器混入两套 State、View 或 Facet。

Mermaid 使用 `mermaid@12.1.0` 与 `patches/mermaid@12.1.0.patch`，修复纯 SVG
模式下圆形等思维导图节点文字向右偏移的问题。编辑、阅读和 Word 图片导出共用此修复。
独立打包时仍需保留该 Mermaid 补丁；它针对默认 ESM 入口，不适用于独立浏览器脚本。

## 挂载、编辑与保存

```vue
<script setup lang="ts">
import { shallowRef, ref } from 'vue'
import { MdEditor, type MdEditorInstance } from '@quirekit/md-editor/vue'
import '@quirekit/md-editor/styles.css'
import { QuireButton } from '@quirekit/ui/vue'

const product = shallowRef<MdEditorInstance | null>(null)
const dirty = ref(false)
const initialMarkdown = '# 发布计划\n\n- [ ] 验证当前版本\n'

function save() {
  const editor = product.value?.editor
  if (!editor) return
  const markdown = editor.getMarkdown()
  // 将 markdown 交给应用自己的保存接口；成功后清除 dirty。
  console.log(markdown)
}
</script>

<template>
  <MdEditor ref="product" :markdown="initialMarkdown" locale="zh-CN" @change="dirty = true" />
  <QuireButton :disabled="!dirty" @click="save">保存</QuireButton>
</template>
```

`markdown` 只作为挂载时的初始值，不是双向绑定。`change` 发出原始 `MsfeChangeEvent`
增量事件，不在每次输入后复制全文。换文档使用公开实例的 `replaceDocument()`；
版本化外部更新使用 `applyChanges()`。完整规则见 [读取与保存](/guide/saving)。

## 错误提示与原文保留

`MdEditor` 用公共 `QuireAlert` 显示创建或运行错误，提示文案跟随 `locale`。
创建失败时不会展示空白编辑器，而是通过 `QuireProseSource` 展示可复制的初始 Markdown。
已经创建的实例遇到扩展或监听器错误时保留当前文档、选区与撤销历史；关闭提示不会重建实例。
请先保存原文，再由应用决定是否在修正配置后重新挂载。

通过 `@error="handleEditorError"` 接收原始异常并接入应用自己的诊断服务。这个事件的参数
为 `unknown`，不把内部错误代码或堆栈当作用户提示。组件处理的是编辑器创建与 MSFE
`onError` 上报的错误，不会屏蔽应用其他位置的 Vue 错误或构建工具诊断。

## 配置产品能力

| 属性 | 用途 |
| --- | --- |
| `locale` | 控件语言，`zh-CN` 或 `en`；不翻译文档正文 |
| `label` | 编辑区的可访问名称 |
| `view` | `document`（默认）、`preview`、`source` 或 `reading`；四种视图共用一份源码与历史，可响应式更新 |
| `readOnly` | 叠加在前三种视图上的只读；只读时仍可展开折叠块和复制 |
| `mentions` | 静态联系人数组或可取消的异步搜索函数；提供后启用提及菜单和输入 `@` 的候选面板 |
| `documentLinks` | 宿主文档链接配置：`search` 为静态文档数组或可取消的异步搜索（关键词为空时可返回最近文档），提供后输入 `[[` 弹出文档搜索并插入标准链接 `[标题](地址)`，“链接”对话框里也能搜索文档；`recognize` 识别指向宿主文档的地址，这类链接显示为带文档图标的引用；`preview` 返回文档的 Markdown 摘要，悬停引用时以缩略正文预览；`exportUrl` 决定导出 Markdown、Word 等文件时文档链接写成的地址，例如补全为完整网址 |
| `toolbar` | 内置工具栏：`true` 或 `'fixed'`（默认）常驻正文上方；`'floating'` 在桌面端选中文字时于选区上方浮出样式、行内格式与链接；`false` 不显示。窄屏在非 `false` 时都使用底部工具坞 |
| `selectionToolbar` | 固定工具栏时，桌面端选中文字也在选区上方浮出样式、行内格式、链接与“添加评论”，默认开启；只读时只保留“添加评论” |
| `blockControls` | 挂载时启用块手柄、菜单与拖动排序，默认开启 |
| `slashMenu` | 空段落中输入 `/` 搜索并插入内容，默认开启，可动态关闭 |
| `placeholder` | 可编辑的空文档中显示的提示，默认「输入 / 插入内容，或直接开始写作」；传空字符串隐藏，挂载时读取 |
| `docx` | Word 转换器与转换策略、图片回调，如 `{ converter: mdDocx() }`；未提供或 `false` 时隐藏 Word 导入、导出项 |
| `exporters` | 宿主安装的导出格式（`MdExporter` 列表），如 PDF、OFD；每项在“导出”菜单中增加一个条目，复用所选模板和节点转换策略 |
| `layout` | `embedded` 为嵌入式边框；`document` 为全宽工具栏与居中文档 |
| `outline` | 在 `document` 布局中显示跟随阅读位置的标题大纲，默认关闭；宽度足够时固定在编辑区边缘，可收起成按钮再展开，宽度不足时点按钮弹出；正文独立居中，空间不足时避让大纲和评论 |
| `outlinePosition` | 大纲位置：`start`（默认，正文左侧，右侧留给评论栏）或 `end`（正文右侧，在评论栏之后） |
| `features` | 按 StarterKit 的同名选项替换或关闭功能 |
| `containers` | 容器配置，`false` 关闭；`types` 替换完整名称列表 |
| `extras` | 按 `math`、`mermaid`、`textDiagram`、`footnotes`、`definitionList`、`subscript`、`superscript`、`insertedText`、`highlight`、`frontmatter` 替换或关闭默认扩展；`false` 全部关闭 |
| `extensions` | 额外 MSFE 扩展；不要重复安装已由产品提供的同名扩展 |
| `codeLanguages` | 默认内置按需语言目录；传入后替换目录，`[]` 禁用，未注册语言保留纯文本 |
| `editingBoundaries` | 挂载时按语法节点覆盖或新增边界规则；`[]` 保留功能默认策略，详见 [光标与编辑边界](#光标与编辑边界) |
| `inlineBoundaries` | 兼容的行内规则列表；默认代码内外分离，其他格式直接进入正文；`[]` 移除此列表，链接、块和组件规则仍生效 |

能力与语言目录在挂载时安装。切换功能集合时显式重新挂载，并由宿主保留需要的文档；
`view`、`readOnly` 可直接更新。更换整套界面语言建议按 locale 设置组件 `key`。

行内边界配置接受 `InlineBoundaryRule[]`，每条声明语法 `nodes`、开闭标记 `marker` 和
`behavior`：`split` 保留内外插入位置，`content` 跳过重叠的边界停顿，`native` 使对应行内规则退出公共边界处理。
默认目录 `mdInlineBoundaries` 可从 `@quirekit/md-editor` 导入后复制修改；Vue 组件与
`createMdEditor` 使用同一选项。文档模式隐藏标记时生效，源码揭示、拖选与组合输入保持原有行为。
该选项和底层 `inlineBoundaryNavigation(rules)` 现在均适配到公共编辑边界运行时；新功能优先使用 `addEditingBoundaries()`。
空代码块的输入占位和默认语言列表见 [代码块定制](/extensions/code-blocks)。

内置工具栏直接使用公共 `QuireToolbar`，与 Classic 共用分组、提示、选中态、方向键导航和
窄屏滚动。正文/标题使用与 Classic 相同的带边框选择器，文字格式平铺；插入菜单分组展示，
每项复用公共图标。菜单打开时捕获可映射的源码锚点，操作回到同一份 Markdown，参与原有撤销历史。

| 功能 | 实际写入与交互 |
| --- | --- |
| 正文与标题 | 正文、六级标题，保留原始 Markdown 文本 |
| 行内格式 | 粗体、斜体、删除线、行内代码，以及 `++下划线++`、`==高亮==`、`H~2~O`、`x^2^` 扩展 |
| 列表 | 对当前段落或选中行切换项目、编号、任务列表；嵌套缩进、回车继续列表、退格退出 |
| 链接与图片 | 对话框编辑地址、文字与标题；图片使用 `QuireProseImage`，不把资源内容写进文档 |
| 表格 | 在当前行上方/下方插入、删除当前行；在当前列左侧/右侧插入、删除当前列；按列设置左/中/右/默认对齐 |
| 代码与容器 | 代码语言、复制；提示、警告、折叠、高亮块和嵌套容器，内部正文直接编辑 |
| 公式 | 行内 `$…$` 与块级 `$$…$$`；复用公共公式编辑器进行结构化输入 |
| 文本绘图 | Mermaid、PlantUML、Graphviz/DOT 共用源码工作区；基础模板、实时预览、SVG/PNG 下载 |
| 注释与术语 | 原子插入脚注引用和定义，引用/定义之间跳转；支持定义列表 |
| 分隔线、日期与表情 | `---` 块、日历选择的 `YYYY-MM-DD` 普通日期、原始 Unicode 表情字符 |
| 提及 | 联系人由宿主提供；写入可移植的 `[@姓名](<地址>)` 链接 |
| 文件 | Markdown、Word 共用导入预览，可替换全文或追加末尾；导出 Markdown、Word 或宿主启用的 PDF/OFD |

下划线、高亮、公式、脚注和自定义容器属于扩展语法，接收 Markdown 的其他应用也需要
相应插件。GFM 表格没有合并单元格语法；本产品不会把合并、列宽、字体大小或文字颜色
伪装成标准 Markdown。云文档、评论、投票等服务也不属于这些源码命令。

`document` 布局用 `--md-editor-toolbar-top` 避开宿主固定页头，默认 `0px`；
工具栏吸附在宿主自己的滚动区内时，用 `--md-editor-panel-top` 单独指定右侧面板的顶部位置（默认同前者）；
`--md-editor-document-width` 控制正文最大宽度，默认 `960px`。外部工具栏可设置
`:toolbar="false"`，经 `product.editor` 调用公开命令；不要直接修改内容 DOM。
宿主自己的菜单可调用实例的 `openFile(mode, exporter?)`，打开与内置“导入 / 导出”相同的窗口：
`mode` 为 `markdownImport`、`wordImport`、`wordExport` 或 `formatExport`（后者用 `exporter` 指定已安装的
导出插件，如 `pdf`、`ofd`）；有其他任务打开或该项未启用时忽略。

`header` 插槽在正文上方、与正文同宽对齐的位置放置宿主内容，如文档标题输入框或作者信息；
`footer` 插槽对应地放在正文下方，如点赞或评论入口。两者都随正文一起滚动，不属于可编辑文档，也不写入 Markdown。

```vue
<MdEditor
  :markdown="initialMarkdown"
  :containers="{ types: ['note', 'warning', 'details', 'decision'] }"
  :features="{ image: false }"
  :extras="{ mermaid: false }"
  :code-languages="[
    { name: 'typescript', load: () => import('@quirekit/cm-lang-javascript').then(m => m.javascript({ typescript: true })) },
  ]"
/>
```

自定义代码 UI 可通过 `features.codeBlock` 传入 `ProseCodeBlock.extend({ addNodeView() { … } })`，
保留产品的 Prose 框架和源语言命令，再替换控制组件。默认公式和文本绘图的替换应放在
`extras.math` / `extras.mermaid` / `extras.textDiagram`，不要在 `extensions` 重复安装同名功能。新的图表语言
如 ECharts 则经 `extensions` 加入，详见 [代码块](/extensions/code-blocks)、[图表](/extensions/diagrams) 与
[Markdown 插件](/extensions/markdown-plugins)。

## 高亮块

高亮块是左侧带表情图标、浅色背景的正文块，适合突出一条提醒或结论。可以在空段落输入 `/高亮块`
（或 `/glk`、`/callout`），也可以从工具栏“插入”或块菜单的“上方插入 / 下方插入”创建。块内是普通正文，
可以继续输入、换段、放列表或嵌套其他块。

- 点击图标打开表情面板，选中后只更换开头一行里的 `emoji`。
- 打开高亮块的块菜单，选择“颜色设置”：“高亮块颜色”提供浅橙、浅红、浅绿、浅蓝、浅紫、浅灰 6 种背景，
  “字体颜色”可选默认或同一组颜色。每次修改是一次可撤销的源码替换。
- 只读状态下图标仅展示，不能更换；实时预览下光标进入高亮块会显示完整源码。

```markdown
::: highlight {emoji=📌 color=purple text=blue}
块内是普通 Markdown，可以有 **强调**、列表或多个段落。
:::
```

属性写在开头一行的 `{}` 里：`emoji` 缺省为 `📌`；`color` 为背景色名，缺省 `orange`；
`text` 为字体颜色名，缺省时不写。颜色只保存色名，编辑和阅读时映射到主题的语义颜色，深色主题自动适配；
未知的键或色名按缺省值显示，源码保持原样。不支持 `:::` 容器的渲染器会把围栏行显示为普通文字，
正文按普通 Markdown 显示，内容不会丢失。

高亮块属于 `containers` 的默认名称 `highlight`。自定义 `containers.types` 时会替换整个名单，
需要高亮块就把 `highlight` 一起写上。`MdProse` 阅读视图、Word、PDF 与 OFD 导出使用同一份源码：
导出时图标作为首行、背景与字体颜色按固定的浅色近似值写入，重新导入 Word 时恢复原始源码。

## 日期、表情与联系人

“插入 → 日期”直接使用公共 `QuireCalendar`。浏览月份不会写入，选择一天或“今天”后才
插入 ISO 普通日期，不保存时区或时间戳。“插入 → 表情符号”使用公共 `QuireEmojiPicker`，
顶部提供常用快捷项，各分类在一个 `QuireScrollArea` 内连续滚动，使用组件库自定义滚动条。
搜索框和底部分类栏固定；点击分类定位对应区域，滚动同步高亮。支持中英文关键词搜索、
搜索结果去重和本次编辑器会话的最近选择。保存的是 Unicode 字符，组合表情
不会被拆开；所有这些插入都写到菜单打开时捕获的位置，可用一次撤销恢复。

提及必须由宿主提供候选联系人；独立示例中的小林、小陈、小许仅是本地示例数据。静态数组
按姓名、描述或 id 过滤；异步函数负责搜索，可以按业务权限限制候选结果：

```vue
<script setup lang="ts">
import { MdEditor } from '@quirekit/md-editor/vue'
import type { MdMention, MdMentionSource } from '@quirekit/md-editor'

const initialMarkdown = '# 项目计划\n\n'
const contacts: readonly MdMention[] = [
  { id: 'lin', label: '小林', url: '/people/lin', description: '设计' },
  { id: 'chen', label: '小陈', url: '/people/chen', description: '开发' },
]

// 可用此函数替代 contacts。接口由应用实现，返回同样的 MdMention[]。
const searchContacts: MdMentionSource = async ({ query, signal }) => {
  const response = await fetch(`/api/people?q=${encodeURIComponent(query)}`, { signal })
  if (!response.ok) throw new Error('联系人暂时不可用')
  return await response.json() as MdMention[]
}
</script>
<template>
  <MdEditor :markdown="initialMarkdown" :mentions="contacts" />
</template>
```

提供 `mentions` 后，可以选择“插入 → 提及”，也可以在正文的空白或开括号后输入 `@`。
候选面板内输入搜索词；方向键和 Enter 或点击选择联系人。输入 `@` 触发时只将该字符替换为
链接；菜单入口替换原选区。取消保留已输入的 `@`，搜索草稿不写入文档。粘贴、邮箱地址、
代码、公式和链接内部不会触发提及。该能力在挂载时启用，运行中更换 provider 可更新候选来源；
从未配置状态改为启用时需重新挂载。

候选请求通过共享 `SuggestionController` 取消和关联；旧查询结果不能覆盖新查询，组合输入
期间不发送查询，失败有重试入口。目标被外部修改、换文档或变为只读时拒绝写入。每个联系人
的 `id` 必须稳定、`label` 非空、`url` 是通过安全校验的目的地。当前提及保存为普通 Markdown
链接，不维护隐藏的人员身份数据，不发送通知。通知、权限和人员详情页面由宿主负责。

通用 `QuireEmojiPicker` 可独立用于任何 QuireKit 界面，经 `@quirekit/ui/vue` 导入。
`items` / `groups` 替换目录，`common` 自定义常用快捷项顺序（`[]` 隐藏），`recent` 由宿主
管理并单独显示。默认常用是精选快捷项，不按频次排序。`select` 发出包含精确 `value` 的条目。
它不依赖 MD Editor、不创建浮层、不自行存储使用记录。默认目录为常用精选集合，不代表
Unicode 表情全集，也不包含肤色选择器。

## 把正文文本显示为链接

工单号、需求号这类短标识可以显示为链接而不改动正文：`@quirekit/msfe-link` 的 `textLinks()` 按宿主给出的
规则识别可见区域中的文本，经 `extensions` 安装。保存的 Markdown 仍是原文，例如 `CLOUD-15084`。

```ts
import { textLinks } from '@quirekit/msfe-link'

const issueLinks = textLinks({
  id: 'issues',
  pattern: /[A-Z][A-Z0-9]+-\d+/u,
  href: key => `https://issues.example.com/browse/${key}`,
  // 可选：只链接真实存在的标识。同一批可见候选合并为一次调用，结果按文本缓存。
  verify: async (keys, signal) => {
    const response = await fetch(`/api/issues?keys=${keys.join(',')}`, { signal })
    return response.ok ? await response.json() as string[] : []
  },
  // 可选：悬停卡片。MD Editor 用统一样式渲染，内容为纯文本。
  card: async (key, signal) => {
    const issue = await fetch(`/api/issues/${key}`, { signal }).then(response => response.ok ? response.json() : null)
    return issue && { subtitle: `${key} · ${issue.type}`, title: issue.summary,
      status: { label: issue.status, tone: 'info' }, fields: [{ label: '经办人', value: issue.assignee }] }
  },
})
```

识别出的文本与普通链接外观一致，带 `data-msfe-text-link` 属性，编辑时 Cmd/Ctrl+点击、只读时单击跳转。
代码、已有链接、图片、HTML 与公式中的文本不识别，前后紧邻字母或数字的匹配也不识别；源码视图保持原样。
校验失败的候选不缓存，之后重新校验；光标仍停在末尾、尚未校验的候选等光标离开后再校验，避免逐字查询。
把同一个 options 对象传给多个编辑器即可共享校验结果。提供 `card` 后，悬停链接 300ms 显示卡片：
副标题、标题、最多三行说明、状态标签（`tone` 可选 neutral、info、success、warning、danger）、短字段和
「打开链接」入口；返回 null 不显示。再提供 `fromHref(url)` 把地址映射回文本，写成普通 Markdown 链接的
同一目标（如 `[需求说明](https://issues.example.com/browse/CLOUD-9)`）也显示这张卡片。
设置 `exportLinks: true` 后，导出 Markdown、Word、PDF、OFD 时识别出的文本写成标准链接，其他软件中也可点击；
编辑器中的正文不变。宿主自己的导出可调用 `linkifyTextLinks(editor.state)` 得到同样的结果。卡片结果按文本缓存，失败的请求下次悬停重试。导出时转为真链接由宿主实现。

## 评论

评论由商业包 `@quirekit/md-editor-comments` 提供（开发与评估免费，生产使用需要授权码）：把 `mdComments()` 传给 `comments` 即可打开，
并引入它的 `styles.css`。选中文字后用工具栏“添加评论”、浮动选区工具栏或 ⌘/Ctrl + Alt + M 留下评论，
可以回复、编辑、删除自己的消息、标记解决。选中的评论卡片上有上一条 / 下一条按钮和当前位置，工具栏“评论”菜单也能跳转或查看全部；解决一条后自动跳到下一条未解决的评论。
评论不写进 Markdown：原文保持干净，评论以标准数据交换格式由应用自己保存。

```vue
<script setup lang="ts">
import { shallowRef, watch } from 'vue'
import { MdEditor, type MdEditorInstance } from '@quirekit/md-editor/vue'
import { mdComments } from '@quirekit/md-editor-comments'
import '@quirekit/md-editor-comments/styles.css'
import { getComments } from '@quirekit/msfe-comments'

const props = defineProps<{ markdown: string; saved?: unknown }>()
const product = shallowRef<MdEditorInstance | null>(null)
const comments = mdComments({
  user: { id: 'lin', name: '小林' },                 // 新消息与解决的署名；null 时只读
  canResolve: thread => thread.messages[0]!.author.id !== 'admin', // 可选：由应用决定谁能解决
  onChange: change => saveComments(change.snapshot()), // 每次评论操作后保存
})
// 挂载后导入已保存的评论；原文改动过也会按原文引用重新定位，找不到的进入“找不到原文”分组。
watch(() => product.value?.editor, editor => { if (editor && props.saved) getComments(editor)?.importComments(props.saved) })
// 正文编辑只移动评论范围、不逐次通知；保存文档时一并导出，锚点保持最新。
function save() { return { markdown: product.value?.editor?.getMarkdown(), comments: product.value?.editor && getComments(product.value.editor)?.exportComments() } }
declare function saveComments(data: unknown): void
</script>
<template>
  <MdEditor ref="product" layout="document" :markdown="markdown" :comments="comments" />
</template>
```

宽屏的文档布局在右侧页边显示评论，卡片对齐所评论的文字；窄屏、嵌入布局和手机通过“评论”
打开列表。提供 `mentions` 时，评论输入框中输入 `@` 也会列出联系人，保存为与正文相同的
`[@名称](<链接>)`，并在消息的 `mentions` 中列出，供应用发送通知。评论正文支持粗体、斜体、
行内代码、删除线、链接和提及，其他 Markdown 按原文显示。

`canComment: false` 只允许查看；`canResolve` 决定是否显示“解决 / 重开”，未提供时不限制；自己的消息总能删除，
`canDelete` 让管理员等角色也能删除别人的消息（不能编辑）。`avatar` 按作者返回头像地址，没有时显示姓名缩写。
这些设置只控制界面，权限必须由应用的服务端再次校验。只读文档也可以评论。评论不随 Word、PDF、
OFD 导出，导入 Word 时批注不会转成评论。交换格式、重新定位与协同计划见
`@quirekit/msfe-comments` 的类型说明。

除了划词评论，也可以对整篇文档发表**全文评论**：工具栏“评论”菜单或评论列表里的“添加全文评论”。
全文评论没有原文位置，回复、解决、@ 提及与划词评论相同，列在“全部评论”顶部的“全文评论”分组；
设置 `documentComments: "end"` 时，正文末尾还会显示全文评论区。含全文评论的数据以交换格式 v2 导出，
不含时仍是 v1。

需要和支持 [CriticMarkup](https://github.com/CriticMarkup/CriticMarkup-toolkit) 的工具交换文件时，
设置 `criticMarkup: true`：“导出”中多出“导出 Markdown（含评论）”，把评论写成
`{==原文==}{>>@作者: 评论<<}`；导入 Markdown 时识别这些标记并转成评论，正文中不保留标记。
编辑器里的原文和评论数据仍然分开，标记只出现在导出的文件里。也可以分别配置：

```ts
const comments = mdComments({
  user: { id: 'lin', name: '小林' },
  criticMarkup: { export: true, import: true, includeResolved: false, suggestions: 'keep' },
})
```

`suggestions` 决定 `{++新增++}`、`{--删除--}`、`{~~旧~>新~~}` 修订标记在导入时原样保留、全部接受
还是全部拒绝。评论落在代码里或跨行时改为写在旁边的单独批注，不会破坏原有语法。不用编辑器时，
也可以直接调用 `@quirekit/msfe-comments/critic-markup` 的 `exportCriticMarkup` 与 `importCriticMarkup`。

## AI 助手

`@quirekit/md-editor-ai` 的 `createMdEditorAi()` 返回 `panel`、`extensions` 与 `destroy()`：
将前两项交给 `MdEditor`，在宿主页卸载时调用 `destroy()`。助手使用内置模型设置、文档工具与改动审批；
`ownerDocument` 提供页面文档，`storage` 提供浏览器存储，`images` 可复用宿主图床。

团队统一管理模型时，传入 `connection: () => Promise<QuireAiModelConnection>`，首次打开面板才加载
授权模型目录。此时隐藏个人连接设置，也不会读取本地保存的 provider 配置；模型选择和审批偏好仍可保存。
管理员页面可复用 `createQuireAiProviderSettings({ persistence })` 的表单，由 `initialDraft`、
异步 `save(draft)`、`clear()` 接入宿主服务端。保存失败保留草稿，不会写入浏览器凭据。
服务端使用 `@quirekit/ai-assistant/settings` 验证配置；模型密钥、成员权限和网关鉴权由宿主负责。

## 协作记录

协作记录回答“谁、何时、做了什么”。记录写在文档之外，格式遵循公共协议 `@quirekit/edit-history`，
存储由宿主实现 `ActivityStore`；编辑器只负责采集和展示。安装两部分并传入同一个存储与文档标识：

```ts
import { MdActivity } from '@quirekit/md-editor'
import { mdActivityPanel } from '@quirekit/md-editor/vue'

const activity = MdActivity.configure({
  store, documentId: 'doc-42', epoch: '1',
  actor: { id: user.id, name: user.name, trust: 'local' },
})
const panel = mdActivityPanel({ store, documentId: 'doc-42', local: true })
```

```vue
<MdEditor :markdown="markdown" :extensions="[activity]" :panels="[panel]" />
```

- 同一块里的连续输入在停顿约 3 秒、换块或失焦时合并为一条；粘贴、剪切、撤销、插入块、移动 / 删除块、
  转换格式、修改高亮块颜色与图标各记一条。远端和宿主写入的变化不由本端记录。
- 面板按日期与相近时间分组，可展开明细、查看改动前后的 Markdown 片段、按成员或时间筛选、隐藏系统通知。
  点击条目定位到当前原文：位置变了会定位到相同内容并标明“原位置已变化”，原内容已删除或无法确定
  （例如同一段文字出现多处）时只提示，不会跳到别处；评论记录定位到评论当前标注的文字。
- 单人使用时 `trust` 为 `local`，`local: true` 让面板标明“仅保存在本机”。多人可信记录需要服务端确认后写入，
  通用协同采集适配器尚未提供。宿主也可在服务端确认正文更新后写入公共协议记录，前端只安装
  `mdActivityPanel` 读取，避免与本地采集重复。`@quirekit/edit-history/memory` 的内存实现只用于示例和测试，刷新即丢失；自建存储可用
  `@quirekit/edit-history/conformance` 的规则逐条验证。

### 多人协同时

协同时由服务端确认后写入，客户端不再写存储。服务端安装 Hocuspocus 扩展，作者取自鉴权结果：

```ts
import { createMsfeActivityExtension } from '@quirekit/msfe-collaboration-hocuspocus/server'

const activity = createMsfeActivityExtension({
  resolveActor: context => ({ id: context.user.id, name: context.user.name, trust: 'verified' }),
  resolveDocument: room => ({ documentId: room, epoch: '1' }),
})
// 在自己的存档事务里写入：activity.takeActivity(room)；失败时 activity.restoreActivity(room, events)
```

客户端只为插入块、粘贴、撤销等带标签的操作发送提示，连续输入由服务端从收到的改动中合并：

```ts
import { encodeActivityHint } from '@quirekit/edit-history/writer'
import { relativeActivityResolver } from '@quirekit/msfe-collaboration'

MdActivity.configure({ documentId, epoch, actor: null, store: null,
  hints: hint => session.sendStateless(encodeActivityHint(hint)) })
mdActivityPanel({ store, documentId, resolveTarget: relativeActivityResolver(session.document) })
```

提示与实际改动对不上时，服务端把这次改动记为“编辑了正文”。断线后很快重连仍算同一次协作，不重复记“加入协作”。
评论房间传入 `commentsRoom: room => room.endsWith(':comments')` 后，添加、回复、解决、重新打开与删除评论也会记录，
作者同样取自连接身份；评论房间的记录在保存该房间时用 `takeActivity(评论房间名)` 取出。

服务端记录要有上限，否则会一直增长。协议的保留策略可以同时限制条数与时间：`maxEvents` 只保留每篇文档最近的若干条，
`fullDays` 之后删除改动片段只留摘要，`summaryDays` 之后整条删除。参考内存实现
`createMemoryActivityStore({ retention })` 在每次写入后执行；数据库存储可在写入事务后或定时任务里执行同样的清理，
例如按序号删除超出条数的旧记录。历史版本另外按 `versionThinning` 稀疏化，命名版本不受影响。

### 历史版本

版本是整篇 Markdown 的快照，存储由宿主实现 `VersionStore`。把同一个版本存储同时交给 `MdActivity` 和面板，
面板会多出“版本”页：

```ts
const activity = MdActivity.configure({ store, versions, documentId: 'doc-42', epoch: '1', actor })
const panel = mdActivityPanel({ store, versions, documentId: 'doc-42', local: true })
```

- 可以输入名称保存命名版本，命名版本永久保留；宿主在自己的存档时机调用 `getMdVersions(editor).save()`
  生成自动版本，内容与上一个版本相同时复用它，不会重复存储。
- 点击版本打开只读预览，可切换到“与当前对比”查看逐行差异，也可以复制全文后手动粘回需要的部分。
- “恢复此版本”先把当前内容存为一个版本，再只改写不同的行，作为一次可撤销的编辑并记为“恢复了版本”；
  没改动的行上的评论和协同锚点不受影响。只读时不能恢复。
- 协同时，服务端在存档时生成自动版本，命名版本由宿主的版本接口在服务端写入“保存了版本”记录，
  作者取自鉴权身份；恢复的记录照常由提示配对确认，客户端不直接写记录。暂不提供撤回某一次编辑。

## 使用共享公式编辑器

从“插入”选择行内公式或块级公式；双击已有块级公式（或选中后点操作栏“编辑”）、点击行内公式都会打开同一个面板。
面板直接使用 `@quirekit/math-ui` 的 `MathAuthoringPanel`，沿用 `@quirekit/math` 的
公式文档、模板、结构化输入与可选 LaTeX 输入；产品不实现第二套公式 UI。
显示公式的 KaTeX 资源与编辑面板按需加载。

公式草稿独立于正文。取消不会写入；确认只替换该公式的 LaTeX 内容，保留美元围栏和
外围引用/列表前缀，作为一次撤销操作。行内公式要求单行且不含外层 `$`；多行内容使用
块级公式。原位置在编辑期间被改写、文档被替换或切为只读时，提交被拒绝并保留草稿。

在使用 UnoCSS 的工作区宿主中，除产品和 UI 源码外，也将公共 `math-ui` 源码纳入扫描。
它使用既有公共组件类名。使用预编译样式的宿主应确保这些公共组件样式已经包含。

需要自建公式宿主时，使用 MSFE 的 `Math.configure({ edit })`，再将它交给
`extras.math`。回调给出 `editor`、`range` 和 `kind`；返回 `true` 表示已接管编辑。
不要在异步面板中长期保存裸 `range`：先 `captureAnchor(range)`，确认时取得
`anchor.target()` 并执行 `editor.run(replaceMdPreview(latex), target)`，结束后释放锚点。
这也适用于 `Mermaid.configure({ edit })`。完整组件生命周期见 [呈现 API](/api/rendering)。

## 图表、链接与图片的修改

“插入 → 文本绘图”打开 Classic、Block、MSFE 共用的源码工作区，支持 Mermaid、PlantUML、Graphviz/DOT。语言切换保留各自草稿，模板须显式应用；预览成功后可保存或下载 SVG/PNG，失败时保留草稿供修改，取消不改正文。标准 Markdown 围栏不保存独立图题，因此该宿主的工作区不显示标题输入。

保存生成普通的 `mermaid`、`plantuml` 或 `dot` 围栏，也能重开 `puml` / `graphviz` 别名。编辑保留原围栏风格及外围块前缀，必要时增长分隔符防止源码关闭外层围栏；切换语言才更新语言名。`extras.mermaid: false` 关闭 Mermaid；`extras.textDiagram: false` 同时关闭 PlantUML/DOT。未安装的语言保留为普通代码。引擎本地运行，外部 include、可选 PlantUML 图库与 D2 未启用，详见[文本绘图接入](/extensions/diagrams)。

把光标放在链接中再点工具栏“链接”可编辑该链接。点击图片会整体选中并浮出操作栏，可编辑、预览、复制、剪切或删除；双击图片直接预览。
若未改变显示文字，链接内部的粗体等原始写法会保留。修改引用式链接的单个使用位置
会将该位置改为行内链接，共享引用定义和其他使用位置不变。危险协议被拒绝。

图片地址保存在 Markdown；私有附件、上传接口或对象 URL 的获取应由宿主提供。
可以用 `Image.configure({ resolve })` 保留图片来源合同，再覆盖 `addNodeView` 接入
自己的公共图片组件。解析器返回资源的释放仍由 MSFE 节点生命周期负责，详见
[图片与资源生命周期](/extensions/resources)。

提供 `images`（`MdImageStorage`）后，宿主的存储负责新图片，正文只写返回的地址：

```ts
const images: MdImageStorage = {
  upload: async (file, { signal, source, name }) => (await uploadToHost(file, { signal, name })).url,
  // 可选：导出 Word / PDF / OFD 时把地址取回为 PNG/JPEG data URI。
  resolve: async (url, signal) => await fetchAsDataUrl(url, signal),
}
```

- 图片对话框出现「上传本地图片」，上传后填入地址与替代文字，仍由用户确认插入。
- 粘贴截图或拖入图片文件时直接上传，完成后插入到粘贴或落下的位置；上传期间的编辑不影响落点。
  剪贴板同时带文字（如复制网页）时走普通粘贴。上传失败不写入任何内容，失败提示由宿主在 `upload` 中给出。
- 流程图、思维导图、白板保存时，预览图以 `source: 'diagram'` 上传，嵌入块只写地址（`preview.url`）；
  上传失败时预览图仍内联保存，不影响这次编辑。导出 Word / PDF / OFD 时经 `resolve` 取回预览图。
- `docx.storeImage` / `docx.resolveImage` 未设置时，Word 导入与导出使用同一个存储。
- 只读编辑器不上传；未提供 `images` 时保持原有行为（只能填写地址，图表预览图内联为 base64）。

## 调用产品源码命令

工具栏也是公开命令的消费者。需要自定义入口时，使用同一个编辑实例与事务：

```ts
import {
  editMdDestination, insertMdFootnote, replaceMdPreview,
  toggleMdInlineCode, toggleMdList, insertMdDate, insertMdMention, insertMdText,
} from '@quirekit/md-editor'

editor.run(toggleMdList('ordered')) // 转换当前段落/选区
editor.run(toggleMdInlineCode())
editor.run(insertMdDate('2026-10-16'))
editor.run(insertMdText('🎯'))
editor.run(insertMdMention({ id: 'lin', label: '小林', url: '/people/lin' }))
editor.run(insertMdFootnote('补充说明')) // 引用与定义在同一事务中
editor.run(editMdDestination(false, { url: '/docs' })) // false 为链接，true 为图片

editor.commands.insertTableRow('before')
editor.commands.insertTableColumn('after')
editor.commands.setTableColumnAlignment('center')
editor.commands.deleteTableRow()
```

`replaceMdPreview(body, language?)` 要求目标是已识别的公式或文本绘图节点；可选语言用于切换现有绘图围栏的语法，它不负责创建新块。
表格命令要求光标或选区属于同一个表格，列对齐只修改分隔行中对应的一个字段，保留
其他单元格原文。检查 `result.ok`；只读、过期目标、不适用的结构都会拒绝修改。

不要直接调用命令规划器后写 DOM，也不要绕过 `editor.run()` 的目标、权限、历史检查。
外部弹层应捕获目标并按生命周期释放，示例与 API 见 [命令与事务](/api/commands)。

## 导入与导出

工具栏的上下双箭头图标打开 **导入 / 导出** 菜单，悬停可查看名称。
菜单按方向分组：导入 Markdown、Word，导出 Markdown、Word。
宿主通过 `exporters` 安装的格式（如 PDF、OFD）各增加一项导出；未安装的格式不显示。
各导入、导出项使用带 MD、W、PDF、OFD 标记的文件类型图标，禁用项的图标同步弱化。

“导入 Markdown”接受文本或最大 2 MB 的本地 `.md/.markdown/.txt` 文件。文件通过
公共 FileUpload 选择、校验与移除，先读入草稿，去除 UTF-8 BOM、统一 CRLF/CR 为 LF。
草稿使用公共 CodeBlock 预览并复制，也可点击“编辑内容”修改或粘贴 Markdown。

点击“替换全文”使用草稿替换当前文档内容；点击“追加到末尾”保留原文并将草稿追加在末尾，
不依赖当前选区。两种操作均可一次撤销；窗口打开期间原文被修改、文档被替换或变为只读时，
保留草稿并禁止写回。若业务需要打开另一份文档并重置身份与历史，应由宿主处理保存后调用
`replaceDocument()`。

“导出 Markdown”读取当前权威原文，不从 Prose DOM 或 JSON 反向生成内容。保存到后端
仍属于宿主职责；在只读状态下也可导出。

## Word 导入与导出

**导入 / 导出** 菜单中的 Word 操作由宿主安装的转换器提供。转换器 `mdDocx()` 位于商业包
`@quirekit/md-editor-export`，基础编辑器只定义 `MdDocxConverter` 合同；未安装时 Word 导入、导出项隐藏：

```ts
import { mdDocx } from '@quirekit/md-editor-export'

const docx = { converter: mdDocx() }
// <MdEditor :docx="docx" />
```

转换运行时仅在用户操作后加载。
导入接受最大 20 MB 的 `.docx` 文件，先转换为 Markdown 草稿，通过 CodeBlock 预览并显示报告。
它与 Markdown 导入共用“替换全文”“追加到末尾”两个按钮，均可一次撤销。
取消、转换失败、导入期间原文变更或文档变为只读时不会写回。
导出捕获打开窗口时的源码快照，转换完成后查看报告、下载 Word；只读状态仍可导出。

首版支持正文、1–6 级标题、粗体/斜体/删除线/行内代码、链接、代码块语言、基础嵌套列表、
带边框和表头底纹的简单表格、原生脚注、公式和 PNG/JPEG 图片。代码块保留语言和换行，并用底纹、边框内距增加留白。
任务状态变为可见勾选字符；默认容器和 Mermaid
使用下面的节点转换扩展。导入时复杂表格、
字体和版式会降级，批注、页眉页脚等省略并报告；导出可通过模板配置新的页眉页脚。
Word 格式和原始 Markdown 写法不保证无损。
可识别的修订删除文字被排除，插入文字保留；这不等同于完整的 Word“接受全部修订”。

行内和块级公式复用 `@quirekit/math-omml`，导出为可编辑的 Word 原生 OMML。重新导入时
只接受通过严格结构核对的 LaTeX；不支持的源公式保留源码，无法反推的 Word 公式保留可读数学文字，
均提供诊断。复杂公式属性、公式外围 Markdown 格式和脚注中的公式不保证保真。

脚注引用和正文写入真正的 Word 脚注，重新导入恢复为 Markdown 引用和定义，保留基础段落、
粗体、斜体等文字格式。重复引用同一定义时，导出为独立 Word 脚注并报告；未引用定义保留
为正文注释。注释内的链接保留文字和 URL，图片保留替代文字，复杂块保留源码并报告。
导入 Word 尾注会转为 Markdown 脚注并报告位置差异。普通脚注标签会规范化，不保证原标签拼写。

转换也可以独立使用 `@quirekit/markdown-docx`，无需挂载编辑器：

```ts
import { exportMarkdownDocx, importDocxMarkdown } from '@quirekit/markdown-docx'

const exported = await exportMarkdownDocx('# 文档\n\n**正文**')
// exported.data 是 Uint8Array；diagnostics 列出近似和丢弃。
const imported = await importDocxMarkdown(exported.data)
console.log(imported.markdown, imported.diagnostics)
```

`markdownToLitAst()` 与 `litAstToMarkdown()` 暴露同一中间转换阶段，方便宿主复用。
前者的 `authoredParts` 承载样式及文档设置；自行编译时必须随 `ast` 一起传入
`createDocxPackagePlan(ast, { authoredParts })`，确保样式与往返元数据采用同一份结果。
`lossPolicy: 'strict'` 会在发现损失时抛出带 `diagnostics` 的 `MarkdownDocxLossError`；
默认 `best-effort` 返回内容和报告。该适配包属于 QuireKit 商业组件，MSFE 的现有许可不变。

默认不请求外部图片：导出直接支持 PNG/JPEG data URI，其它来源需要显式
`resolveImage(source): Promise<string | null>` 返回这两种格式的 data URI。
默认模板使用 A4 页面和四边 1 英寸页边距；图片保持原始比例、只缩小不放大，按正文、列表
或嵌套单元格的实际可用范围限制尺寸。标题默认 1.1 倍行距、段后 4–6 磅，正文 1.15 倍行距、
段后 6 磅，列表项段后 2 磅；自定义转换已提供的间距或样式引用优先。
相邻表格和带框容器之间补充 8 磅的块间分隔，与已有段距共同决定可见留白；
嵌套容器内的同级块遵循相同规则，重新导入时不会因此增加空白内容。
这些外观现在可以通过导出模板配置；省略模板时保留上述默认值。
导入默认保留 data URI；可通过
`storeImage({ dataUrl, alt }): Promise<string | null>` 保存图片并返回持久 URL。
保存失败保留内嵌数据并报告，临时 `blob:` 地址不会进入结果。将这些选项与转换器一起传给 `MdEditor`
的 `docx` 属性，或传给独立转换函数。
在编辑器之外转换（例如宿主把 Word 文件导入为新文档）时，调用
`mdDocx().importDocx(data, { policy, features, signal })`，它使用与编辑器相同的默认转换（流程图、思维导图、
白板、容器与代码绘图），导入导出才能往返一致。宿主自定义节点转换通过 `mdDocx({ extensions })` 安装。
Word 原生图表在 Markdown 中没有对应语法，导入时按图表缓存数据渲染为静态 PNG（随后同样经
`storeImage` 保存），每张图表报告一条 `chart-rasterized`；无法读取的图表族（散点、组合图、新式图表等）
替换为 `[替代文字]` 占位并报告 `chart-omitted`，不会让整个文件导入失败。宿主可通过
`mdDocx({ renderChart })` 改用自己的渲染器（签名为 `(chart) => Promise<string | null>`），`builtins: false` 时不渲染图表。

### PDF 与 OFD 导出

PDF 与 OFD 由可选商业包 `@quirekit/md-editor-export` 以导出插件的形式提供，基础编辑器不依赖它：

```ts
import { mdOfdExport, mdPdfExport } from '@quirekit/md-editor-export'

const exporters = [mdPdfExport(), mdOfdExport()]
// <MdEditor :exporters="exporters" />
```

每个插件在“导出”菜单中增加一项。导出时捕获当前源码与所选模板，生成后显示页数和转换报告，再由用户
下载；只读文档也可导出。导出窗口中的“更换”使用同一份源码重新生成，关闭或切换文档会废弃过期结果。
转换器与字体在第一次导出时才加载。

两种格式都通过临时 Classic 文档完成真正分页，再分别编码原生 PDF 与原生 OFD（不经 PDF 中转）。
正文是可选择的文字；表格、提示块、脚注、页眉页脚和动态页码使用现有排版能力，图表为图片。
公式当前使用默认数学字体输出高清图形并报告这一降级，其文字不能单独选择。自定义节点复用
[已有转换器](/guide/export-nodes)，不需要为每种格式再维护一套节点转换。

插件默认使用 `@quirekit/font-pack` 的便携字体：同一套字节先安装为浏览器字体用于度量，再嵌入文件。
OFD 没有标准 Courier，插件为常见代码字体嵌入 DejaVu Sans Mono 并报告替代，许可原文随 font-pack 发布。
PDF 中代码字体没有对应资源时，先解析为等宽后备字体再排版并报告。宿主可通过 `fonts` 选项替换整套字体，
通过 `metadata` 设置标题、作者等文档信息。缺字或资源失败均有诊断；`lossPolicy: 'strict'` 拒绝有损输出。

不需要编辑器界面时，可直接调用 `@quirekit/md-editor-export/pdf` 的 `exportMarkdownPdf(markdown, options)`
或 `/ofd` 的 `exportMarkdownOfd`，返回 `data`、`pageCount` 和 `diagnostics`。这时需显式提供可嵌入字体；
无浏览器环境必须提供 `textMeasurer`，包含公式时还需 `rasterizeFormula`。OFD 的 `fontEmbedding`
默认 `subset`，可设为 `full`。

两种格式都不承诺与 Word 在不同字体环境下逐像素一致，也不保留可往返编辑的 Markdown 元数据，不提供
Markdown 导入。PDF/A、Tagged PDF 与大文档后台线程属于后续能力。

### 导出模板

`@quirekit/export-template` 提供格式中立的配置、默认模板与严格校验，使用 MIT 许可。
模板只用于导出，不更改 Markdown 内容、选区或撤销历史，也不影响 Word 导入。
页面使用 mm；字号、段距、边框和内距使用 pt；行距明确区分倍数与固定值。下面是公开的合成配置示例：

```ts
import { resolveExportTemplate } from '@quirekit/export-template'
import { exportMarkdownDocx } from '@quirekit/markdown-docx'

const template = resolveExportTemplate({
  fonts: { body: { latin: 'Arial', eastAsia: '微软雅黑' } },
  body: { after: 5 },
  headings: { h1: { border: { color: '287B69', width: 0.75 }, borderSpace: 4 } },
  footer: { enabled: true, center: '{page} / {pages}' },
})
const file = await exportMarkdownDocx(markdown, { template, documentTitle: '项目记录' })
```

可以设置正文、H1–H6、列表、引用、代码、表格、容器、脚注、公式及图片的外观。
角色字体提供西文、东亚、复杂文字槽位；留空为主题字体。`fonts.math.latin` 对应 Word 的
默认数学字体，原生公式仍可编辑。模板不嵌入字体，不保证所有设备分页一致。
单节默认页眉页脚支持左中右内容及 `{title}`、`{page}`、`{pages}`；后两者为 Word 原生动态域，
需阅读器更新后显示实际页码，过长单行文字会缩略并报告。快速样式示意不是 Word 分页预览。

`MdEditor` 可以直接接收 `:docx="{ converter, template }"`，也可以由宿主接入模板库：

- `templates`：实现 `ExportTemplateProvider` 的目录、精确版本读取与可选保存/删除服务。
- `v-model:exportStyle`：当前文档的 `{ id, revision }`，由宿主与文档设置一起持久化。
- `#export-templates="{ provider, selected, choose, close }"`：安装宿主选择器；确认时调用 `choose`，取消调用 `close`。

同时提供 provider 与 slot 后，图标菜单显示“导出样式”；Word 和已安装的 PDF/OFD 导出自动沿用选择。
导出窗口中的“更换”保持本次源码快照并重新生成文件，关闭或切换文档废弃过期结果。
无持久化宿主时只记住当前实例；已有版本缺失或无权限会阻止导出，要求重新选择，不静默回退。
只读文档仍可选择样式和导出。扩展通过 `markdownExportTemplateService` 读取同一解析配置，
自定义转换明确提供的直接格式和样式引用仍然优先。

现有 [MD Editor 示例](/md-editor) 已接入模板库；中英文示例分别保存导出样式。文档站宿主
采用私有商业授权，基础编辑器和导出协议的授权保持不变。

第一方 `@quirekit/export-template-library` 组件和官方设计包独立采用商业授权，支持浏览、
新建、复制编辑、保存版本与显式本地存储，由商业宿主按上述接缝装配。组件授权与内容包授权分别解析，
支付、账号和生产授权校验由宿主提供；基础 Markdown/Word 导出不依赖商业包。上传 DOCX/DOTX
作为模板仍属于后续能力。

“模板管理”中，“新增模板”从默认配置开始；“基于当前模板新建”复制当前浏览的模板样式。
卡片通过主色边界、轻底色和勾选标记显示选择，点击右上角编辑按钮可修改该模板。
内置模板只读，编辑入口会创建副本并保留来源；个人模板编辑后保存为同一模板的新版本。
“当前使用”标记对应文档已经采用的版本；浏览、编辑和单独保存不会应用，点击“使用此模板”或“保存并使用”才更新文档的导出设置。

复制或编辑模板时，窗口尺寸保持稳定，切换分类后仅更新可滚动的字段区域；模板名称、
分类栏和底部保存按钮始终可见。窄屏使用紧凑预览，空间很低时优先显示编辑字段。
字段按外观、文字、段落等用途分组，数值旁直接显示单位。点击颜色块可选择预设色或打开
“更多颜色”精确调整；文字颜色可恢复“自动”，底色可设为“无填充”。加粗、斜体和对齐使用格式按钮。

### 自定义节点转换与往返

MD Editor 默认注册容器与三种文本绘图转换，开关跟随 `containers` / `extras.mermaid` / `extras.textDiagram`：

| 节点 | Word 中的内容 | 再导入 |
| --- | --- | --- |
| 单段容器 | 段落底纹、四边框与边框内距 | 校验通过时恢复容器源码 |
| 含标题、列表或嵌套块的容器 | 单格表格底纹、边框和单元格内边距，正文仍可编辑 | 校验通过时恢复完整容器 |
| 高亮块 | 表情作为首行，无边框底纹，可选字体颜色 | 校验通过时恢复高亮块源码 |
| Mermaid、PlantUML、Graphviz/DOT | 从源码离屏渲染的 PNG 图片 | 校验通过时恢复原围栏与源码 |

圆角和折叠交互会降级并报告。默认容器解析支持根级及容器正文中的已关闭命名围栏；
列表、引用内部的容器需要宿主扩展解析。图形渲染失败保留代码，不依赖当前视口中是否挂载预览。

源码元数据存入标准 Custom XML part，通过内容控件关联。可见内容、图片字节、脚注/尾注或样式/编号
发生变化后，导入保留当前 Word 内容；不会用过期源码覆盖 Word 修改。元数据丢失、标识重复、
版本不兼容或扩展未安装时也保留原生内容或图片。这里已验证 QuireKit 往返，尚未承诺
Word/WPS 等软件再次保存后必定保留元数据。

`<MdEditor :docx="{ converter, preserveMetadata: false }" />` 可导出纯原生内容和图片；
`builtins: false` 关闭默认节点转换，但保留宿主经 `mdDocx({ extensions })` 安装的转换。基础 MSFE 包没有 DOCX 依赖。

独立适配器按需注册同一套功能。图片渲染由宿主明确提供，不隐式抓取资源：

```ts
import {
  createMarkdownDocxContainerExtension,
  createMarkdownDocxCodeImageExtension,
  exportMarkdownDocx,
} from '@quirekit/markdown-docx'

// renderDiagram 是宿主提供的异步函数，返回 PNG/JPEG data URI 或 null。
const extensions = [
  createMarkdownDocxContainerExtension({ names: ['note', 'warning', 'decision'] }),
  createMarkdownDocxCodeImageExtension({ languages: ['mermaid'], render: renderDiagram }),
]
const result = await exportMarkdownDocx(markdown, { extensions })
// importDocxMarkdown 使用同一 extensions；仅导入时可以省略 render。
```

自定义功能实现 `MarkdownDocxExtension.addDocx()`，返回与 Classic 相同的节点转换贡献。
Word/PDF/OFD 共用 `parse` / `prepare` / 节点 codec 与模板 service；DOCX 的 package pass 和
Custom XML 恢复只适用于 Word。完整的原生业务卡片、图片渲染、组合注册、取消与失败处理见
[自定义节点导出为 Word、PDF 与 OFD](/guide/export-nodes)。

扩展必须校验 payload 版本与字段，并保留可见回退。每条恢复元数据上限为 64 KiB，
文档总计 1 MiB / 1000 条。`preserveMetadata` 控制内置扩展；宿主扩展自行管理其策略。

## 共享 Prose 类名

编辑区在实时和只读状态使用 `qk-prose qk-prose--compact`，`MdProse` 使用同一紧凑密度；
代码块使用 `qk-code-block qk-prose-source`。任务框复用公共 Checkbox 与 `qk-prose-task-marker`
组合类，保持正文行高，并与编辑时的 `[x]` / `[ ]` 共用占位宽度。
容器使用 `qk-prose-container`、`qk-prose-container__header`、`qk-prose-container__body`
和语义 `data-tone`。适配器将 CodeMirror 块映射为原生语义元素或 `data-qk-prose` 角色，
直接消费公共 Prose CSS。普通 DOM 阅读与编辑不各写一套皮肤。

先通过应用的 QuireKit 主题修改字体、色彩和间距。业务需要特殊容器时，在宿主作用域
下为 `.qk-prose-container[data-container="你的类型"]` 提供明确的变体；不要逐行画背景
或替换 CodeMirror 管理的 DOM。需要在静态阅读中复用该类型时，保留相同的容器标识。

## 独立的阅读视图快照

```vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { EditorState } from '@quirekit/cm-state'
import { MdEditor, MdProse, type MdEditorInstance } from '@quirekit/md-editor/vue'
import { QuireButton } from '@quirekit/ui/vue'
const product = shallowRef<MdEditorInstance | null>(null)
const snapshot = shallowRef<EditorState>()
</script>
<template>
  <MdEditor ref="product" markdown="# 同一份文档" />
  <QuireButton @click="snapshot = product?.editor?.state">生成阅读快照</QuireButton>
  <MdProse v-if="snapshot" :state="snapshot" locale="zh-CN" />
</template>
```

`MdProse` 消费不可变状态并生成可丢弃的语义 DOM，不生成另一份可编辑 JSON，也不会
反向序列化 Markdown。已注册容器名称保留为 `data-container`，可与编辑视图共用变体。
阅读适配覆盖核心块、行内扩展、链接、图片、公式、Mermaid、PlantUML、Graphviz/DOT、定义列表、脚注和 Frontmatter。
公式与图表复用同一安全渲染器，元数据保留为源码。未注册的绘图语言仍按普通代码块显示。
第三方扩展可通过 `@quirekit/md-editor/vue` 的 `withMdReading()` 贡献阅读内容，复用原
codec 的字段校验，并由公共层处理资源取消、释放和失败回退，见[自定义节点](/extensions/custom-node)。
未知节点保留源码文本，默认 HTML 也保留为文本。`components` 与显式 `html` 策略共用
[公共 Incremark reader](/guide/incremark) 的合同。阅读贡献不自动提供 Word/PDF/OFD 导出能力。

## 直接显示文本与 AI 流式回复

无需先创建编辑器，直接把当前收到的**完整原文**传给 `content`。每次追加更新同一个字符串，
改写、清空和切换消息也使用这个属性；不要同时传 `state`。

```vue
<MdProse
  :content="message.content"
  :identity="message.id"
  :status="message.receiving ? 'receiving' : 'complete'"
  locale="zh-CN"
  @error="reportReadingError"
/>
```

`status` 接受 `receiving`、`complete`、`stopped`、`error`，只描述传输状态。停止/失败保留
原文，后置定义仍可修正前文；没有提前冻结的“完成块”。未闭合的代码块先展示代码，闭合后
才允许 Mermaid 等预览启动。重复脚注有独立的引用锚点，重复链接定义遵守首次定义优先。

`options` 接收 `MdEditorExtensionOptions`，包括 `extras`、`containers` 和 `extensions`；
阅读扩展仍通过 `withMdReading` 注册。更换 options 对象或 identity 会释放旧会话和资源。
配置必须是无需挂载编辑器的纯配置，不支持在初始化时使用 `this.editor` 或接入 source provider。
错误通过 `MdReadingError` 上报，文本解析/配置失败的 `phase` 为 `parse`，原文作为转义文本保留。

### reading/stream.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from "vue";
import { MdProse, type MdProseProps } from "@quirekit/md-editor/vue";
import { QuireButton, QuireTypography } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import { readingSample } from "./sample";

const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const content = ref(readingSample(props.locale)), identity = ref(0);
const status = ref<NonNullable<MdProseProps["status"]>>("complete");
const labels = computed(() => props.locale === "zh-CN"
  ? { start: "开始流式演示", stop: "停止", load: "一次性加载", reading: "流式阅读预览", receiving: "正在接收", stopped: "已停止", complete: "已完成" }
  : { start: "Start streaming", stop: "Stop", load: "Load all", reading: "Streaming reading preview", receiving: "Receiving", stopped: "Stopped", complete: "Complete" });
let timer: ReturnType<typeof setTimeout> | undefined;

/** Cancels the producer independently of MdProse's parser lifecycle. */
function cancel(): void { if (timer !== undefined) clearTimeout(timer); timer = undefined; }
/** Supplies the authoritative full text after each simulated network chunk; no parser APIs are needed. */
function start(): void {
  cancel(); identity.value++; content.value = ""; status.value = "receiving";
  const source = readingSample(props.locale);
  /** Delivers the next 20 UTF-16 code units, including arbitrary syntax boundaries. */
  function append(): void {
    content.value = source.slice(0, content.value.length + 20);
    if (content.value.length < source.length) timer = setTimeout(append, 30);
    else { timer = undefined; status.value = "complete"; }
  }
  append();
}
/** Retains the exact received prefix when the user stops delivery. */
function stop(): void { cancel(); status.value = "stopped"; }
/** Replaces the message with its complete source and a new resource identity. */
function load(): void { cancel(); identity.value++; content.value = readingSample(props.locale); status.value = "complete"; }
onBeforeUnmount(cancel);
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center gap-2">
      <QuireButton size="sm" variant="outline" @click="start">{{ labels.start }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :disabled="status !== 'receiving'" @click="stop">{{ labels.stop }}</QuireButton>
      <QuireButton size="sm" variant="ghost" @click="load">{{ labels.load }}</QuireButton>
      <QuireTypography as="span" variant="helper">{{ status === 'receiving' ? labels.receiving : status === 'stopped' ? labels.stopped : labels.complete }}</QuireTypography>
    </div>
    <section :aria-label="labels.reading"><MdProse :content="content" :identity="String(identity)" :status="status" :locale="locale" /></section>
  </div>
</template>

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

### reading/sample.ts

````ts
import type { ExampleLocale } from "../shared/types";

/** @param locale - Tutorial language. @returns One source with late definitions, repeated footnotes and an incrementally displayed code block. */
export function readingSample(locale: ExampleLocale): string {
  const chinese = locale === "zh-CN";
  return [chinese ? "## 流式 Markdown" : "## Streaming Markdown",
    chinese ? "先收到正文和[文档][api]引用，定义会在末尾到达。这里重复引用同一脚注[^note]、[^note]。"
      : "The [documentation][api] reference arrives before its definition. This footnote is used twice[^note], [^note].",
    ...Array.from({ length: 6 }, (_, index) => chinese
      ? `第 ${index + 1} 段保留 **强调**、中文与 emoji 🙂。已收到的正文继续可读，后置定义到达时前面的链接会更新。`
      : `Paragraph ${index + 1} keeps **emphasis** and emoji 🙂. Existing text stays readable while late definitions update earlier links.`),
    "```ts\nconst message = 'hello'\nconsole.log(message)\n```",
    chinese ? "[^note]: 脚注正文也使用[文档][api]链接。" : "[^note]: The footnote also links to the [documentation][api].",
    "[api]: https://example.com/docs", "[api]: https://example.com/ignored",
  ].join("\n\n");
}

````

例子每 30ms 追加 20 个 UTF-16 字符，末尾才给出链接和脚注定义。停止后可以一次性加载，
也可以重新开始。这里的可运行示例使用 `MdProse`；为兼容原合同，SSR 和客户端首帧
都输出转义原文，挂载后再解析。需要服务端语义
DOM 时传入预先接受的 `state` 快照，或使用[公共阅读组件](/guide/incremark)。额外解析工作
协作让出主线程，但不是硬实时预算；大文档
和密集引用仍有全局工作。稳定顶层块在语法、位置与依赖均未变时复用投影；增长中的块仍需重新投影。

无框架宿主可使用 `MdProseStream`，调用 `update(content)` 接受全文、`advance(budget)`
推进解析，再消费 `state`。返回 `false` 时由宿主调度下一个切片；类本身不创建定时器。
不要把 Incremark 的 AST renderer、HTML 执行或图表配置直接传入这个入口。

## 无框架装配

`createMdEditor(options)` 返回原始 `MsfeEditor`，生命周期、命令和保存方法不变。
`createMdEditorExtensions(options)` 返回产品扩展组合，可以交给已有的 MSFE 实例工厂。
Vue 的任务、图片、代码语言选择与折叠按钮由 `vueMdEditorExtensions(locale, options, edit?)`
装配，需要同时使用 MSFE Vue 的组件宿主。第三个参数可接管公式/图表编辑；直接使用
`MdEditor` 时这些步骤及公共弹层已完成。无框架工厂本身不挂载 Vue 工具栏或公式面板；
宿主应提供 `extras.math` / `extras.mermaid` / `extras.textDiagram` 的编辑回调，或使用 `view: 'preview'`
直接编辑活动节点的源码。
