输入规则把用户刚输入的模式转成正文;PM 插件保存事务派生的状态。两者都依附同一个 Editor,无需监听整个页面再手动同步一份文本。
输入规则:键入结束符后转换
本例定义 readingHighlight 标记。键入 ==重点== 的最后一个 = 后,规则去掉两端符号,把“重点”转成该标记。最后一个正则捕获组是保留的文字;末尾的 $ 限制匹配到刚输入的位置。
import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@quirekit/core";
/**
* Creates a small DOM-oriented mark with ==text== typing and paste rules.
* @returns A fresh mark with semantic HTML and persisted tone attributes.
*/
export function createReadingHighlight() {
return Mark.create({
name: "readingHighlight",
/** @returns Persisted semantic tone and its HTML decoding rule. */
addAttributes() {
return { tone: { default: "note",
/** @param element - HTML mark. @returns Supported tone only. */
parseHTML: (element: HTMLElement) => element.getAttribute("data-tone") === "important" ? "important" : "note",
/** @param attrs - Canonical attributes. @returns Stable HTML data attributes. */
renderHTML: (attrs: Record<string, unknown>) => ({ "data-tone": attrs.tone }),
} };
},
/** @returns Only this application's semantic marks are recognized. */
parseHTML() { return [{ tag: "mark[data-reading-highlight]" }]; },
/** @param props - Resolved attributes. @returns A mark containing its text. */
renderHTML({ HTMLAttributes }) { return ["mark", mergeAttributes(HTMLAttributes, { "data-reading-highlight": "" }), 0]; },
/** @returns End-anchored rule, applied after a closing == is typed. */
addInputRules() { return [markInputRule({ find: /==([^=]+)==$/u, type: this.name })]; },
/** @returns Global rule, applied to each match in pasted text. */
addPasteRules() { return [markPasteRule({ find: /==([^=]+)==/gu, type: this.name })]; },
});
}
将 createReadingHighlight() 加入已有 Editor 的 extensions。这里的 tone 是持久属性,默认 note;HTML 使用 data-tone 保存。parseHTML() 限定只读取带本产品标识的 <mark>,不能把任意外部高亮都当作相同业务含义。
例程的 renderHTML() 用于 DOM 与 HTML 转换。Canvas/SVG 需要另外声明该标记的绘制样式;定义 HTML 标签不会自动给画布上色。它也没有声明 Markdown 的 ==...== 转换规则,输入快捷方式与文件转换是两个合同。
粘贴规则:一次处理多个匹配
输入规则通常用末尾锚定表达式;粘贴规则用全局标志 g 匹配本次粘贴中的每个片段。例程的两个规则共享同一标记,因此输入后或粘贴后的 JSON 含义相同。
setContent()、JSON 恢复和服务器导入不应依赖用户输入规则。导入 Markdown、HTML 或文件时,应使用对应格式的解析器。需要关闭规则时,在创建选项中配置 enableInputRules / enablePasteRules,见创建选项。
PM 插件:只保存需要的派生状态
假设你要显示当前选区的位置。createSelectionRange(notify) 把 from / to 存入 PluginKey 对应的状态,通过 readSelectionRange(editor.state.pm) 查询;mounted plugin view 只在范围变化时通知界面。
import { Extension } from "@quirekit/core";
import { Plugin, PluginKey, type EditorState } from "@quirekit/pm";
import type { SelectionRange } from "./types";
const key = new PluginKey<SelectionRange>("docsSelectionRange");
/** @param state - Accepted PM state. @returns The extension's immutable range, if installed. */
export function readSelectionRange(state: EditorState): SelectionRange | undefined { return key.getState(state); }
/**
* Projects selection changes without scanning or serializing the document.
* @param notify - Mounted UI notification; call no document commands here.
* @returns Fresh extension owning its plugin state and mounted observer.
*/
export function createSelectionRange(notify: (range: SelectionRange) => void) {
return Extension.create({
name: "docsSelectionRange",
/** @returns One state owner; no second editor or document is created. */
addProseMirrorPlugins() {
return [new Plugin<SelectionRange>({
key,
state: {
/** @param _config - PM construction options. @param state - Initial state. @returns Initial range. */
init: (_config, state) => ({ from: state.selection.from, to: state.selection.to }),
/** @param tr - Accepted transaction. @param value - Last projection. @returns Reused value unless the range changed. */
apply: (tr, value) => tr.selection.from === value.from && tr.selection.to === value.to
? value : { from: tr.selection.from, to: tr.selection.to },
},
/** @param view - Mounted PM view. @returns A view-scoped observer released with the view. */
view(view) {
notify(key.getState(view.state)!);
return {
/** @param current - Current view. @param previous - Previously accepted state. */
update(current, previous) {
const value = key.getState(current.state)!;
if (value !== key.getState(previous)) notify(value);
},
};
},
})];
},
});
}
创建时安装 createSelectionRange(range => updateSelectionLabel(range)),其中 updateSelectionLabel 是应用的界面函数。from / to 是文档位置,不能用差值当作准确字数:节点边界、UTF-16 和跨块内容都会影响它。
apply() 每次只比较两个位置,不扫描全文,也不发命令。它返回旧对象时,观察者知道界面不需要更新。无视图的 Core 仍有插件状态;view() 及其 UI 通知只在对应视图挂载时存在。
资源和验证
插件若创建 DOM 监听、定时器或外部订阅,在 plugin view 的 destroy() 中清理;与整个 Editor 同寿命的资源可由扩展 onDestroy 清理。不要在 apply() 中启动异步请求,或在观察者里无条件写入事务形成循环。
验证时分别覆盖键入、一次粘贴两个匹配、撤销、JSON 恢复和移动选区。恢复 JSON 不应重新触发正则转换;移动选区只更新派生状态,不改变正文。PM 的 filterTransaction / appendTransaction 会影响全局事务流程,确有需要时再使用,参数见扩展 API。