体验字段修改
点击「待处理」将它切换为「已完成」,下方阅读预览同步改变。再读取 Markdown,确认按钮只改写 [[todo]] 中的状态,其他正文保持原样。撤销与重做同时更新编辑和阅读视图;切换只读后不能修改字段。
定义语法和字段
status.ts 使用 Node.create() 声明语义;addAttributes() 允许 value 字段并校验取值。defineSource() 的字面分隔符语法适合单行、不嵌套的 token。
read(document, node) 只读取已确认范围;不符合约定的值返回 null,继续显示源码。复杂或嵌套语法应通过 addMarkdownExtensions() 提供解析规则。
精确写回
patch(document, node, patch) 返回源码修改。这个例子保留两侧括号,只替换内部值。字段验证、只读、过期目标和撤销由编辑器统一处理。
@quirekit/msfe-core不依赖特定解析器的语义编解码器;渲染是可选能力,不负责持久化。
export interface SourceDefinition<Attrs extends object, Resource = unknown> {
readonly nodes: readonly string[];
readonly syntax?: InlineSyntax;
readonly placement?: "replace" | "before";
readonly showWhenActive?: boolean;
readonly measureHeight?: boolean;
readonly read: (document: SourceDocument, node: SourceNode) => Attrs | null;
readonly source?: (document: SourceDocument, node: SourceNode) => string;
readonly patch: (document: SourceDocument, node: SourceNode, patch: Partial<Attrs>) => SourceEdit | readonly SourceEdit[] | null;
readonly load?: (attrs: Readonly<Attrs>, signal: AbortSignal) => MsfeNodeViewResource<Resource> | Promise<MsfeNodeViewResource<Resource>>;
readonly renderer?: MsfeNodeViewRenderer;
}添加命令
addCommands() 定义 insertStatus();types.ts 的模块增强让 editor.commands、editor.can() 和 editor.chain() 获得类型提示。命令使用提供的 tr,避免自己提交另一笔事务。
editor.commands.insertStatus('todo')
editor.can().insertStatus('done')
editor.chain().insertStatus('todo').insertText(' ').run()
替换组件
应用用 .extend({ addNodeView() { ... } }) 接入 VueNodeViewRenderer()。status-view.vue 接收 nodeView.attrs 和 updateAttributes(),无需自行计算源码偏移。
组件树必须包含 VueNodeViewHost。更换为 React 时保留同一语法扩展,使用对应的 renderer 与 host。
接入静态阅读
MdProse 使用 withMdReading(extension, definition) 接收第三方阅读贡献。先完成扩展的
configure() / extend(),再包装并安装到同一个编辑器。示例使用的公开入口是
@quirekit/md-editor/vue,不需要修改产品内部的语法分支。
import { h } from 'vue'
import { withMdReading } from '@quirekit/md-editor/vue'
import { Status } from './status'
import type { StatusAttributes } from './types'
const ReadableStatus = withMdReading<StatusAttributes>(Status, {
render({ attrs }) {
return h('strong', attrs.value === 'done' ? '已完成' : '待处理')
},
})
render(context, resource) 的 attrs 来自原扩展安装后的 codec,字段默认值与校验只定义
一次。context 同时提供 name、node、document 和 locale,不提供编辑动作。
renderInline(from?, to?) 与 renderBlocks(omitted?) 将子内容交回公共阅读器;行内范围
必须在当前节点内且不能截断一个子语法节点,非法范围会触发局部源码回退。
MdProse 的 state 接收原编辑器的不可变快照。示例用 const state = useMsfeState(editor)
接入 Vue,公共 hook 自动管理订阅与清理,见 Vue 状态订阅。
每次输入不序列化 Markdown,纯选区与焦点变化不重复解码或加载。
阅读只依赖源码、解析树、已安装贡献、呈现配置与 locale;codec 不应读取选区或可变外部状态
来决定静态内容。文档变化可以保守重建阅读快照,这个接口不承诺逐节点增量更新。
阅读资源与失败回退
有 IO 的贡献可声明 load(context, signal),返回 ResourceLease<Resource>,即
{ value, release? },或相应 Promise。render 只在资源就绪后运行。取消信号在快照替换
或卸载时触发;即使加载方忽略取消,迟到资源仍会调用 release()。阅读资源与编辑
NodeView 的资源分别拥有生命周期,不自动重用 addSource().load。
待加载、失败和服务端渲染时保留原节点的精确源码;SSR 不启动加载。同步 renderer 可以 服务端渲染。Vue 子组件继承应用 provider,客户端子组件异常也隔离在本节点。 渲染器是宿主提供的可信代码,原始 Markdown 不会被当作 HTML 执行。
用 <MdProse @error="reportReadingError" /> 接收 MdReadingError,其中包含 owner、
read / load / render / dispose 阶段、原始异常与捕获快照的 UTF-16 范围。
这个范围是诊断信息,不能用于异步写回当前编辑器。无错误监听器时,Vue 阅读入口只保留
可见源码;宿主可按需接入日志。资源释放失败会被报告,不替换已经成功显示的新内容。
| 场景 | 编辑 / 源码 | 静态阅读 |
|---|---|---|
| 合法的自定义字段 | 由原扩展 patch 精确修改,可撤销/重做 | 使用同一个 codec 的默认值与校验结果 |
| 字段非法、未知或未闭合语法 | 保留原文 | 未命中贡献时保留内置展示或源码 |
| 代码块 / 行内代码中的 token | 仍是代码文本 | 不额外解析成自定义节点 |
| 同语法有多个语义候选 | 沿用安装顺序 | 首个 codec 返回非 null 的候选拥有该节点 |
render 返回 undefined / null | 原文不变 | undefined 保留精确源码;null 明确不输出内容 |
| 加载等待、拒绝或渲染异常 | 原文不变,其他块正常 | 仅失败节点回退并可报告错误 |
| 只读模式 | 字段动作与历史写入被拒绝 | 继续展示同一份内容 |
| 没有 source codec / 重复阅读 owner | 明确拒绝安装 | 不创建无效阅读贡献 |
| React 或 Word/PDF/OFD 导出 | 保留中立语义扩展 | 此适配器只负责 Vue 阅读;导出需独立转换贡献 |
生命周期约束
离开视口、显露源码、换文档或销毁时,节点组件可能卸载。持久字段写回源码,临时 UI 状态按合适的生命周期管理;不要依赖组件永远挂载。
read、patch 与字段校验的契约
addAttributes() 是允许写入的字段清单,默认值与 validate 不能替代源码编码。read() 返回 null 表示这个已识别语法不属于该语义形式,原文保留;它不应修复或标准化用户文本。
patch() 接收局部字段,不保证包含所有字段。返回 { from, to, insert }、多个不重叠修改或 null。只替换需要的字段区域,保留空白、引号、其他属性和分隔符风格。如果新值包含分隔符,必须编码或拒绝,不能直接拼入语法。
只传入未知字段或不合法值时,运行时会拒绝。对多种源码拼写(例如单引号、双引号、参考定义)先定义可逆写回策略;不能写回的形式继续可读,不假装支持编辑。
source 指纹何时需要
默认源码身份由相关范围确定。提供 source(document, node) 可给出更小的精确指纹,但必须覆盖所有会被 patch 修改的字段。若控件只修改代码围栏语言,指纹可以只覆盖 header;若控件同时修改正文,就不能仍只比较 header。
指纹用于防止过期组件修改不同内容,不能用作持久业务 ID,也不能用节点在视口内的 id 保存到数据库。需要业务 ID 时把它定义为可校验的源码字段。
从字段到异步组件
defineSource<Attrs, Resource>() 可增加 load(attrs, signal)。组件收到 status: 'loading' | 'ready' | 'error' 和可选 resource;字段变化和卸载由资源协议管理。组件仍通过 updateAttributes()、deleteNode()、revealSource() 等受控动作操作源码。先看 异步资源,再增加上传或服务器查询。
块节点应该如何选择
本页的字面语法是单行、不嵌套 token,NodeView 只拥有这个 token 的显示。syntax helper 不适合冒号嵌套容器或任意跨行语法。容器正文沿用原生 Markdown,见 容器;跨行静态预览见 图表;自定义解析见 Markdown 插件。
检查新增节点是否完整
先验证合法、非法、未闭合和转义形式;再验证字段更新只改变对应源码、撤销恢复原样、只读动作拒绝、快速改字段时旧资源释放、组件卸载后动作失效。最后在窄屏和深浅主题下检视按钮与周围文本的基线、选区和焦点。