Lexical React 编辑器集成 WIRIS MathType 公式编辑器:MathTypeExtension 与 MathTypeNode 实战解析 Lexical React 编辑器集成 WIRIS MathType 公式编辑器MathTypeExtension 与 MathTypeNode 实战解析【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical本指南以 Lexical 仓库中的 react-mathtype 示例 为蓝本讲解如何把 WIRIS MathType/ChemType 公式编辑器桥接到 Lexical React 富文本编辑器公式以自定义MathTypeNode节点形式存储而非让 MathType 直接篡改 contenteditable。读完本文你将掌握 MathType 集成实例的生命周期管理、公式节点的数据模型与 DOM 导入导出、以及扩展输出Extension Output替代 React Context 的原因与做法。示例概览为什么需要桥接而非直接集成MathType 是一套成熟的开源数学公式编辑方案其通用集成包wiris/mathtype-generic默认会直接向页面里的 contenteditable 元素写入内容。这与 Lexical 的架构理念相悖Lexical 要求所有文档变更都发生在editor.update()内部、通过词法节点树完成从而保证撤销/重做、序列化与协同等能力的一致性。因此 react-mathtype 示例 采用了桥接思路MathType 只负责提供公式编辑 UIMathType/ChemType 对话框与工具栏编辑器把 MathType 生成的 MathML 转成自定义MathTypeNode节点状态在editor.update()内提交双击已有公式节点时重新打开 MathType 进行编辑。整个桥接被封装为MathTypeExtension其核心代码位于 MathTypeExtension.tsx。该示例依赖wiris/mathtype-generic版本 ^8.15.2见 package.json并将 MathType 作为普通示例依赖对待。扩展的装配build 闭包持有的可变桥接状态MathTypeExtension使用 Lexical 扩展系统defineExtension定义其build回调在每个编辑器实例生命周期内执行一次创建并私有持有一个MathTypeSession对象// MathTypeExtension.tsx 中的 MathTypeSession 类型 type MathTypeSession { integration: MathTypeIntegrationInstance | null; // MathType 集成实例 pendingNodeKey: NodeKey | null; // 对话框正在编辑的节点 key };这个对象刻意不跨出build闭包扩展对外暴露的是操作它的函数mountIntegration、editFormula而不是对象本身从而保证跨越 React Hook 边界的任何数据都不可变。扩展定义本身MathTypeExtension.tsxexport const MathTypeExtension defineExtension({ build(editor) { const session: MathTypeSession {integration: null, pendingNodeKey: null}; return { Component: MathTypeIntegrationComponent, editFormula: (nodeKey, formula) editFormula(editor, session, nodeKey, formula), mountIntegration: (target, toolbar) mountIntegration(editor, session, target, toolbar), }; }, dependencies: [ReactExtension], name: lexical/react-mathtype-example/MathType, nodes: () [MathTypeNode], // 懒引用避免循环依赖导致的暂时性死区 });两点值得注意dependencies: [ReactExtension]声明了该扩展依赖 React 扩展层nodes使用函数形式懒引用MathTypeNode——因为MathTypeNode模块会反向import本模块调用useExtensionDependency若此处急切引用在MathTypeNode先被 bundle 求值时会触发暂时性死区temporal dead zone错误。mountIntegration创建集成实例并返回清理函数mountIntegration负责完成 MathType 集成实例的创建、初始化与销毁。它在createIntegration内做关键的两步改造再调用init()与listeners.fire(onTargetReady, {})启动集成MathTypeExtension.tsx。拦截 openNewFormulaEditor清除残留的 temporalImageMathType 的GenericIntegration.openNewFormulaEditor()在editionProperties.temporalImage仍被设置时会重新打开已存在的公式编辑器。而editFormula会设置该字段且只有完整插入流程才会清除它——若取消编辑残留值会导致下次新建公式时错误地重开旧公式并二次插入。因此桥接包装了该方法先清空temporalImage再调用原实现integration.openNewFormulaEditor () { session.pendingNodeKey null; integration.core.editionProperties.temporalImage null; wirisPlugin.currentInstance integration; openNewFormulaEditor(); };重写 insertFormula把 MathML 提交为 Lexical 节点这是桥接的核心MathType 完成编辑后回调insertFormula桥接将其转化为对 Lexical 节点树的更新MathTypeExtension.tsx。其处理逻辑分为三种情况空 MathML用户清空/删除公式ContentManager.submitAction()在对话框以空公式确认时新建空公式点 Accept、或擦除已有公式会调用updateFormula(null)而 Accept 按钮从不被禁用。此时若pendingNodeKey存在则删除对应节点MathML 被 showimage 服务拒绝畸形公式wirisPlugin.Parser.mathmlToImgObject返回null直接放弃提交文档保持原样正常 MathML通过createFormulaFromImage组装MathTypeFormula在editor.update()中调用$commitFormula提交并用onUpdate回调在调和reconciliation完成后把焦点还给编辑器。$commitFormula的提交策略MathTypeExtension.tsx若有nodeKey且节点仍是MathTypeNode则setFormula原地更新否则在 RangeSelection 处insertNodes插入新节点若连选区都没有则追加到根节点的新段落中。焦点管理与清理对话框关闭时MathType 的取消流程会把焦点移到离屏的编辑目标元素上导致光标丢失。桥接注册了onModalClose全局监听器在对话框关闭后调用editor.focus()把焦点还给编辑器。由于Listeners只提供add()没有remove()清理函数需要手动从wirisPlugin.Core.globalListeners.listeners数组中splice移除该监听器再清空工具栏内容并调用integration.destroy()。MathTypeIntegrationComponent离屏目标与工具栏的渲染MathTypeIntegrationComponent是扩展的Component通过useExtensionDependency(MathTypeExtension).output拿到mountIntegration在useEffect中挂载集成MathTypeExtension.tsx。它渲染两个元素工具栏容器div.mathtype-toolbar离屏编辑目标MathType 需要一个 contenteditable 元素来锚定对话框但本示例的公式都提交给 Lexical 节点因此该元素被定位到屏幕外position: fixed; left: -10000px见 styles.css同时aria-hiddentrue隐藏于辅助技术tabIndex{-1}使其不可被键盘聚焦——一个 contenteditable 元素默认可聚焦而 aria-hidden 元素绝不能可通过键盘到达。组件需要在编辑器内部挂载使用方式为ExtensionComponent lexical:extension{MathTypeExtension} /见 App.tsx。为什么用扩展输出而非 React Context示例 README 特别强调了一个架构决策使用扩展自身的输出useExtensionDependency(MathTypeExtension).output而非 React Context 来传递editFormula等函数。原因是ReactExtension暴露了唯一的EditorChildrenComponent插槽last-one-wins见 ReactExtension.tsx 中的DefaultEditorChildrenComponent与 types.ts 中ReactConfig的注释说明若 MathType 扩展覆写它来注入自己的组件会静默破坏编辑器里其他同样需要该插槽的扩展。扩展系统自带的依赖注入能力让 MathType 可以在不占用这一单例插槽的前提下把桥接函数分发给MathTypeNode的装饰器组件。MathTypeNode公式的 Lexical 节点数据模型MathTypeNode继承DecoratorNodeJSX.Element用 React 组件MathTypeFormulaComponent渲染公式MathTypeNode.tsx。节点通过$config()声明了 6 个扁平状态字段类型定义见 MathTypeData.ts状态字段含义解析规则mathML公式的 MathML 源码字符串否则空串src公式渲染图片的 URL字符串否则空串altText替代文本可访问性字符串否则空串width/height图片宽高数字否则nullcustomEditor关联的自定义编辑器名如 ChemType字符串否则nullcreateDOM生成span.editor-mathtype作为装饰容器updateDOM返回false表示 DOM 不做增量更新。选中态由MathTypeFormulaComponent通过useLexicalNodeSelection管理点击节点选中Shift点击切换选中时容器加.selected类描边样式见 styles.css。装饰器渲染的img带有 MathType 解析器期望的完整标记classNameWirisformula、data-mathml经safeXmlEncode编码、data-custom-editor、rolemath以及可选的width/height。双击且编辑器可编辑时调用editFormula(nodeKey, formula)重新打开 MathTypeMathTypeNode.tsx。getTextContent()返回altText || mathML保证公式可被纯文本导出与搜索。DOM 导入导出img.Wirisformula 形状的还原MathTypeNode的$config()中通过importDOM: buildImportMap({...})声明了对img元素的转换凡命中isWirisFormulaImageIMG且带Wirisformula类或data-mathml属性的节点以优先级 3 转换为MathTypeNodeMathTypeNode.tsx。与之对称exportDOM()调用createImageFromFormulaMathTypeData.ts输出 MathType 解析器期望的img.Wirisformula形状设置alignmiddle、类名、src、alt、rolemath、编码后的data-mathml可选data-custom-editor、width、height并强制style.maxWidth none。值得注意的是isWirisFormulaImage用nodeName IMG而非instanceof HTMLImageElement判断因为剪贴板或 iframe 中的节点可能来自另一个 realm见 MathTypeData.ts。同样地createImageFromFormula始终显式传入归属的Document而不是读取document全局确保元素创建在编辑器的 realm 内——这与仓库 AGENTS.md 中关于 Shadow DOM 与 iframe 的注意事项一致。解析公式图片与 editFormula 回开createFormulaFromImage从 MathType 生成的图片元素提取全部字段优先使用调用方传入的fallbackMathML否则从data-mathml属性经safeXmlDecode解码parseOptionalNumber只接受有限且大于 0 的数值否则返回null。editFormula则反向操作MathTypeExtension.tsx设置pendingNodeKey把公式还原成temporalImage标记dbclick true、isNewElement false并根据formula.customEditor启用或禁用 MathType 的自定义编辑器ChemType 场景最后调用openExistingFormulaEditor()。若集成尚未挂载integration null返回false表示无法打开。环境与运行网络服务、许可证与构建体积示例根目录的 package.json 定义了脚本运行方式如下仓库根目录先pnpm installpnpm install pnpm run dev # 启动 Vite 开发服务器 pnpm run build # tsc 类型检查后执行 vite build pnpm run typecheck # tsc --noEmit 仅做类型检查在 monorepo 内部开发时使用pnpm run monorepo:dev加载vite.config.monorepo.ts其中引入仓库的 lexicalMonorepoPlugin 以解析各lexical/*包源码。严格类型检查由 tsconfig.json 开启strict、noUnusedLocals、noUnusedParameters、noFallthroughCasesInSwitch均为true。关于运行前提需要注意三点网络依赖开箱状态下集成调用 WIRIS 托管的演示服务www.wiris.net公式图片渲染依赖 showimage 服务编辑器必须能访问该服务才能渲染公式许可证生产环境使用 MathType 可能需要 WIRIS 许可证或自托管服务服务配置细节需参照 MathType 通用集成文档构建体积wiris/mathtype-generic在模块顶层被急切导入见 MathTypeExtension.tsx因为它会安装window.WirisPlugin单例——MathTypeNode首次渲染公式时就需要它存在。这使整个 MathType bundle 进入首屏 chunk因此vite build会报告超过 500 kB 的 chunkREADME 已明确说明这是该方案的预期代价。小结react-mathtype 示例展示了在 Lexical 扩展系统下桥接第三方内容编辑器的一套完整范式build闭包私有持有可变桥接状态并暴露纯函数式输出、insertFormula回调把外部编辑结果转译为editor.update()内的节点提交、自定义DecoratorNode承载公式数据并提供与 MathType 解析器兼容的 DOM 导入导出最后通过扩展输出而非 React Context 分发依赖规避EditorChildrenComponent单例插槽冲突。这套模式同样适用于将其他自管理 DOM的第三方编辑器公式、图表、音视频等以可控方式嵌入 Lexical。参考链接示例说明 README.md扩展实现 MathTypeExtension.tsx节点实现 MathTypeNode.tsx数据模型 MathTypeData.ts全局类型 MathTypeGlobals.ts【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考