前言:前面的文章【element-tiptap】实现公式编辑 中,已经实现了一种非常简单的公式编辑,键入latex公式直接在文档中转换。今天讲的另一个更加复杂的公式编辑器的扩展,双击公式的时候弹出公式编辑的弹窗,可以对公式进行可视化编辑。
公式编辑器,是文本编辑中必不可少的一项功能,俺这里有一个开源的公式编辑器项目 mathquill,界面比较的简单清晰,可以尝试一下将这个mathquill加入到我们的编辑器中。首先看一下WPS中公式编辑器的界面,是在插入的下拉菜单中
是一个弹出框的形式。插入的下拉框咱们先不管,先做一个按钮,点击的时候弹出这个模态框
一、增加模态框
1、增加扩展 src/extensions/formula-editor.ts
import type { Editor } from '@tiptap/core';
import { Extension } from '@tiptap/core';
import FormulaEditorDialog from '@/components/menu-commands/formula-editor.dialog.vue';const FormulaEditor = Extension.create({name: 'formulaEditor',addOptions() {return {...this.parent?.(),button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {return {component: FormulaEditorDialog,componentProps: {editor},};},};},
});export default FormulaEditor;
2、导出扩展 /src/extensions/index.ts
export { default as FormulaEditor } from './formula-editor';
3、图标文件 src/icons/formula-editor.svg
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M10.9954 9.48532V9.48532C11.7434 10.8632 12.4934 12.4986 14.0082 12.9025C14.1745 12.9468 14.3394 12.9706 14.5 12.9706M10.9954 9.48532V9.48532C10.2479 8.10819 9.52602 6.40326 7.99805 6.05589C7.83534 6.0189 7.66687 6 7.49086 6M10.9954 9.48532V9.48532C11.7438 8.10667 12.4343 6.35565 13.9727 6.04913C14.1435 6.01511 14.3184 6 14.5 6M10.9954 9.48532V9.48532C10.2473 10.8635 9.65752 12.8449 8.09596 12.989C8.08334 12.9902 8.07063 12.9912 8.05782 12.9921C7.23416 13.0479 6.88097 12.8029 6.5 12.4359" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path><path d="M15.0026 2.25H5.02037C4.86672 2.25 4.73791 2.36609 4.72199 2.51892L3.5 14.25L1.75 12L1 12.75" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>
4、弹出框组件 src/components/menu-commands/formula-editor.dialog.vue
<template><div><command-button:is-active="editor.isActive('formulaEditor')":command="openDialog":enable-tooltip="enableTooltip":tooltip="t('editor.extensions.FormulaEditor.tooltip')"icon="formula-editor":button-icon="buttonIcon"/><el-dialogv-model="formulaEditorDialogVisible":title="t('editor.extensions.FormulaEditor.dialog.title')":append-to-body="true"width="400px"class="el-tiptap-edit-link-dialog">ffffffffff<template #footer><el-button size="small" round @click="closeDialog">{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}</el-button><el-buttontype="primary"size="small"round@mousedown.prevent@click="addLink">{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}</el-button></template></el-dialog></div>
</template><script lang="ts">
import { defineComponent, inject } from 'vue';
import {ElDialog,ElForm,ElFormItem,ElInput,ElCheckbox,ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';export default defineComponent({name: 'FormulaEditor',components: {ElDialog,ElForm,ElFormItem,ElInput,ElCheckbox,ElButton,CommandButton,},props: {editor: {type: Editor,required: true,},buttonIcon: {default: '',type: String},placeholder: {default: '',type: String}},setup() {const t = inject('t');const enableTooltip = inject('enableTooltip', true);return { t, enableTooltip };},data() {return {formulaEditorDialogVisible: false,};},watch: {},methods: {openDialog() {this.formulaEditorDialogVisible = true;},closeDialog() {this.formulaEditorDialogVisible = false;},addLink() {// this.editor.commands.setLink({ href: this.linkAttrs.href });this.closeDialog();},},
});
</script>
5、src/i18n/locales/zh/index.ts 和 src/i18n/locales/zh-tw/index.ts
FormulaEditor: {tooltip: '公式编辑',dialog: {title: '公式编辑',cancel: '取消',confirm: '确认'},
}
FormulaEditor: {tooltip: '公式编辑',dialog: {title: '公式编辑',cancel: '取消',confirm: '確認'},
}
二、引入公式编辑器
mathquill
把源码下载下来以后,首先需要执行 npm install
安装依赖
安装完依赖之后,根目录有一个 quickstart.html,将这个文件右键在浏览器中打开
会出现一个比较简陋的页面
我们看一下这个文件的代码,其中引入的依赖文件有三个
<link rel="stylesheet" type="text/css" href="build/mathquill.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.js"></script>
<script src="build/mathquill.js"></script>
所以我们先把 build 文件夹放到我们的 【element-tiptap】项目中
然后在 index.html
中引入这几个文件
index.html
<link rel="stylesheet" href="./src/utils/mathquill/mathquill.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.js"></script>
<script src="./src/utils/mathquill/mathquill.js"></script>
然后在下拉框组件中 src/components/menu-commands/formula-editor-dialog.vue 把代码复制过去
<el-dialogv-model="formulaEditorDialogVisible":title="t('editor.extensions.FormulaEditor.dialog.title')":append-to-body="true"width="400px"class="el-tiptap-edit-link-dialog"
><p>Static math span:<span id="static-math">x = \frac{ -b \pm \sqrt{b^2-4ac} }{ 2a }</span></p><p>Editable math field: <span id="math-field">x^2</span></p><p>LaTeX of what you typed: <code id="latex">x^2</code></p><p><a href="http://docs.mathquill.com/en/latest/Getting_Started/">MathQuill’s Getting Started Guide</a></p><template #footer><el-button size="small" round @click="closeDialog">{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}</el-button><el-buttontype="primary"size="small"round@mousedown.prevent@click="addLink">{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}</el-button></template>
</el-dialog>
渲染需要一些时间,所以这里先写了一个延时函数
openDialog() {this.formulaEditorDialogVisible = true;setTimeout(()=>{var staticMathSpan = document.getElementById('static-math');var mathFieldSpan = document.getElementById('math-field');var latexSpan = document.getElementById('latex');var MQ = MathQuill.getInterface(2); // keeps the API stable// easily create static or editable math from a DOM element by calling the// appropriate constructor: http://docs.mathquill.com/en/latest/Api_Methods/MQ.StaticMath(staticMathSpan);// you may pass in an options object:var mathField = MQ.MathField(mathFieldSpan, {spaceBehavesLikeTab: true, // an example config option, for more see:// http://docs.mathquill.com/en/latest/Config/handlers: {edit: function () {console.log("mathField.latex()")console.log(mathField.latex())// retrieve, in LaTeX format, the math that was typed:latexSpan.textContent = mathField.latex();},},});}, 300)
},
然后就有了
三、可编辑的latex
需要有一个编辑区域,可用来输入latex,然后生成公式
<template><div><command-button:is-active="editor.isActive('formulaEditor')":command="openDialog":enable-tooltip="enableTooltip":tooltip="t('editor.extensions.FormulaEditor.tooltip')"icon="formula-editor":button-icon="buttonIcon"/><el-dialogv-model="formulaEditorDialogVisible":title="t('editor.extensions.FormulaEditor.dialog.title')":append-to-body="true"width="800px"class="formula-editor-dialog"><div class="latex-input-section"><div class="input-area"><span id="math-field" class="math-input"></span></div><div class="latex-preview"><span>LaTeX:</span><code id="latex">x^2</code></div></div><template #footer><el-button size="small" round @click="closeDialog">{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}</el-button><el-buttontype="primary"size="small"round@mousedown.prevent@click="insertFormula">{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}</el-button></template></el-dialog></div>
</template><script lang="ts">
import { defineComponent, inject } from 'vue';
import {ElDialog,ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';export default defineComponent({name: 'FormulaEditor',components: {ElDialog,ElButton,CommandButton,},props: {editor: {type: Editor,required: true,},buttonIcon: {default: '',type: String},placeholder: {default: '',type: String}},setup() {const t = inject('t');const enableTooltip = inject('enableTooltip', true);return { t, enableTooltip };},data() {return {formulaEditorDialogVisible: false,mathField: null as any,MQ: null as any,currentLatex: '',};},methods: {openDialog() {this.formulaEditorDialogVisible = true;this.initMathQuill();},initMathQuill() {setTimeout(() => {const mathFieldSpan = document.getElementById('math-field');const latexSpan = document.getElementById('latex');this.MQ = MathQuill.getInterface(2);this.mathField = this.MQ.MathField(mathFieldSpan, {spaceBehavesLikeTab: true,handlers: {edit: () => {this.currentLatex = this.mathField.latex();latexSpan.textContent = this.currentLatex;},},});}, 300);},closeDialog() {this.formulaEditorDialogVisible = false;this.currentLatex = '';},insertFormula() {if (this.currentLatex) {// this.editor.commands.insertFormula({ latex: this.currentLatex });}this.closeDialog();},},
});
</script><style lang="scss" scoped>
.formula-editor-dialog {.latex-input-section {padding: 20px;.input-area {margin-bottom: 16px;.math-input {display: block;width: 100%;min-height: 60px;border: 1px solid #dcdfe6;border-radius: 4px;padding: 8px;}}.latex-preview {font-size: 14px;color: #606266;}}
}
</style>
挺 6 的,因为在编辑区域编辑的时候能够直接生成可编辑的公式
四、将公式插入编辑器
在 【element-tiptap】如何把分隔线改造成下拉框的形式? 一文中,我们有探索了怎么新建一个在编辑器中可以显示的DOM节点,可以先浏览一下。
1、创建插入的组件
向编辑器中插入内容的时候,需要使用 node-view-wrapper
标签包裹起来,我们首先需要创建一个这样子的组件,作为我们插入公式的时候实际插入的内容
src/components/extension-views/formula-view.vue
<template><node-view-wrapper as="span" class="formula-view"><span v-html="getFormulaHtml()" :latex="latex" @dblclick="editFormula"></span></node-view-wrapper>
</template><script lang="ts">
import { defineComponent } from 'vue';
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3';export default defineComponent({name: 'FormulaView',components: {NodeViewWrapper,},props: nodeViewProps,computed: {mathml(): string {return this.node!.attrs['mathml'];},latex(): string {return this.node!.attrs['latex'];},},methods: {getFormulaHtml(){return this.mathml;},getLatex(){return this.latex;},editFormula(){this.editor.commands.editFormula({ latex: this.latex });}},
});
</script><style>
.formula-view .mq-editable-field.mq-focused,
.formula-view .mq-math-mode .mq-editable-field.mq-focused {box-shadow: none;border: none;cursor: pointer;
}
.formula-view .mq-editable-field .mq-cursor {display: none;
}
.formula-view .mq-math-mode:hover {box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;border-color: #709AC0;
}
</style>
2、改造 formulaEditor 扩展
需要增加两个命令,一个是往编辑器中插入公式,一个是编辑公式,因为公式双击的时候需要进入公式编辑器进行编辑。需要额外说一下,有两个属性:inline: true,group: 'inline',
,有了这两个属性,公式插入之后才能在行内显示,否则会独占一行。
src/extensions/formula-editor.ts
import { Node, mergeAttributes } from '@tiptap/core';
import { Editor, VueNodeViewRenderer } from '@tiptap/vue-3';
import FormulaEditorDialog from '@/components/menu-commands/formula-editor.dialog.vue';
import FormulaView from '@/components/extension-views/formula-view.vue';declare module '@tiptap/core' {interface Commands<ReturnType> {formula: {setFormula: (options: { src: string }) => ReturnType;};}
}const FormulaEditor = Node.create({name: 'formulaEditor',// schemainline: true,group: 'inline',selectable: false,addAttributes() {return {...this.parent?.(),src: {default: null,parseHTML: (element) => {const src = element.getAttribute('src');return src;},},latex: {default: null,parseHTML: (element) => {const latex = element.getAttribute('latex');return latex;},},mathml: {default: null,parseHTML: (element) => {const mathml = element.getAttribute('mathml');return mathml;},},};},parseHTML() {return [{tag: 'span',},];},renderHTML({ HTMLAttributes }) {return ['span',mergeAttributes(HTMLAttributes, {}),];},addCommands() {return {setFormula:(options) =>({ commands }) => {return commands.insertContent({type: this.name,attrs: options,});},editFormula:(options) => {this.editor.emit('openFormulaEditor', options);}};},addOptions() {return {button({ editor }: { editor: Editor }) {return {component: FormulaEditorDialog,componentProps: {editor,},};},};},addNodeView() {return VueNodeViewRenderer(FormulaView);},
});export default FormulaEditor;
3、改造公式编辑器弹出框组件
- 点击 确认 按钮的时候,调用命令
setFormula
插入公式; - 监听
openFormulaEditor
,双击公式的时候打开公式编辑器弹窗
<template><div><command-button:is-active="editor.isActive('formulaEditor')":command="openDialog":enable-tooltip="enableTooltip":tooltip="t('editor.extensions.FormulaEditor.tooltip')"icon="formula-editor":button-icon="buttonIcon"/><el-dialogv-model="formulaEditorDialogVisible":title="t('editor.extensions.FormulaEditor.dialog.title')":append-to-body="true"width="800px"class="formula-editor-dialog"><div class="latex-input-section"><div class="input-area"><span id="math-field" class="math-input"></span></div><div class="latex-preview"><span>LaTeX:</span><code id="latex">{{ currentLatex }}</code></div></div><template #footer><el-button size="small" round @click="closeDialog">{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}</el-button><el-buttontype="primary"size="small"round@mousedown.prevent@click="insertFormula">{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}</el-button></template></el-dialog></div>
</template><script lang="ts">
import { defineComponent, inject, onMounted, getCurrentInstance } from 'vue';
import {ElDialog,ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';export default defineComponent({name: 'FormulaEditorDialog',components: {ElDialog,ElButton,CommandButton,},props: {editor: {type: Editor,required: true,},buttonIcon: {default: '',type: String},placeholder: {default: '',type: String}},setup(props) {const t = inject('t');const enableTooltip = inject('enableTooltip', true);onMounted(() => {console.log(props.editor);// 获取当前组件const currentComponent = getCurrentInstance();const openDialog = currentComponent?.ctx.openDialog;props.editor.on('openFormulaEditor', (options: { latex: string }) => {openDialog(options.latex);});});return { t, enableTooltip };},data() {return {formulaEditorDialogVisible: false,mathField: null as any,MQ: null as any,currentLatex: '',};},methods: {openDialog(latex: string) {this.formulaEditorDialogVisible = true;this.initMathQuill(latex);},initMathQuill(latex: string) {setTimeout(() => {const mathFieldSpan = document.getElementById('math-field');const latexSpan = document.getElementById('latex'); this.MQ = MathQuill.getInterface(2);if(latex) {this.setLatex(latex);}this.mathField = this.MQ.MathField(mathFieldSpan, {spaceBehavesLikeTab: true,handlers: {edit: () => {this.currentLatex = this.mathField.latex();latexSpan.textContent = this.currentLatex;},},});}, 300);},closeDialog() {this.formulaEditorDialogVisible = false;this.currentLatex = '';if (this.mathField) {this.mathField.latex('');}},insertFormula() {const mathml = document.getElementById('math-field')?.outerHTML?.replace(/id="math-field"/g, '');this.editor.chain().focus().setFormula({mathml,latex: this.currentLatex}).run();this.closeDialog();},setLatex(latex: string) {this.currentLatex = latex;if (this.mathField) {this.mathField.latex(latex);} else {this.$nextTick(() => {if (this.mathField) {this.mathField.latex(latex);}});}},},
});
</script><style lang="scss" scoped>
.formula-editor-dialog {.latex-input-section {padding: 20px;.input-area {margin-bottom: 16px;.math-input {display: block;width: 100%;min-height: 60px;border: 1px solid #dcdfe6;border-radius: 4px;padding: 8px;}}.latex-preview {font-size: 14px;color: #606266;}}
}
</style>
五、编辑公式
当前已经可以实现向编辑器中插入公式,并且可以双击编辑器中的公式的时候,进入公式编辑器进行公式编辑。但是还有一个问题,就是编辑公式点击确认的时候,会重新向编辑器中加入一个新的公式,而不是替换之前的旧的公式。
我有一个大胆的想法。你看,在输入文字的时候,如果你鼠标选中了几个文字,然后输入,那么选中的文字就会直接被删了,那么能不能双击公式的时候,给公式新建一个 selection,让它是选中状态?然后再插入的时候,直接把现有的删了。显然,是不行的,还是应该双击的时候,记录下来当前的DOM元素,然后编辑公式之后操作指定的DOM元素。
给 src/components/extension-views/formula-view.vue 组件中,公式元素增加双击的绑定方法
<template><node-view-wrapper as="span" class="formula-view"><span ref="mathField" :latex="latex" @dblclick.stop="editFormula"class="formula-content"></span></node-view-wrapper>
</template><script lang="ts">
import { defineComponent } from 'vue';
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3';
import katex from 'katex';export default defineComponent({name: 'FormulaView',components: {NodeViewWrapper,},props: nodeViewProps,computed: {latex(): string {return this.node?.attrs?.latex || '';},},methods: {editFormula(event: MouseEvent) {event.preventDefault();event.stopPropagation();const target = event.currentTarget as HTMLElement;if (!target) return;requestAnimationFrame(() => {this.editor.commands.editFormula({latex: this.latex,currentEle: target});});},renderFormula() {if (this.$refs.mathField && this.latex) {try {katex.render(this.latex, this.$refs.mathField as HTMLElement, {throwOnError: false,displayMode: true});} catch (error) {console.error('Error rendering formula:', error);}}}},mounted() {this.renderFormula();},updated() {this.renderFormula();}
});
</script><style>
.formula-view {display: inline-block;cursor: pointer;user-select: none;
}.formula-content {display: inline-block;padding: 2px 4px;pointer-events: auto;
}.formula-view:hover .formula-content {background-color: rgba(0, 0, 0, 0.05);border-radius: 4px;
}
</style>
六、支持使用快捷方式输入公式
我们的latex公式,通常都是使用双$
或者单$
包裹的。在扩展中,我们需要制定输入规则以及粘贴,当检测到输入的内容有双$
或者单$
包裹时,自动转换为公式。
1、输入规则
输入规则通过扩展的 addInputRules
属性规定,返回一个对象数组,对象中包含 find
和 handler
,find
指明要匹配的正则表达式,handler
指明要进行的处理方法。
src/extensions/formula-editor.ts
addInputRules() {return [// 双$规则{find: DOUBLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {const latex = match[1];if (!latex) return null;const { tr } = state;const start = range.from;const end = range.to;tr.delete(start, end);tr.insert(start, this.type.create({latex}));},},// 单$规则{find: SINGLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {const latex = match[1];if (!latex) return null;const { tr } = state;const start = range.from;const end = range.to;tr.delete(start, end);tr.insert(start, this.type.create({latex}));},},];},
2、粘贴规则
如果粘贴的文本中有公式,也需要进行转换。
addPasteRules() {return [{find: DOUBLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {const latex = match[1];if (!latex) return null;const { tr } = state;const start = range.from;const end = range.to;tr.delete(start, end);tr.insert(start, this.type.create({latex}));},},{find: SINGLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {const latex = match[1];if (!latex) return null;const { tr } = state;const start = range.from;const end = range.to;tr.delete(start, end);tr.insert(start, this.type.create({latex}));},},];
},addProseMirrorPlugins() {return [new Plugin({key: new PluginKey('formulaEditorPaste'),props: {handlePaste: (view: EditorView, event: ClipboardEvent, slice: Slice) => {const content = event.clipboardData?.getData('text/plain');if (!content) return false;// 查找所有的公式const doubleMatches = Array.from(content.matchAll(DOUBLE_DOLLAR_REGEX));const singleMatches = Array.from(content.matchAll(SINGLE_DOLLAR_REGEX));if (doubleMatches.length === 0 && singleMatches.length === 0) {return false; // 如果没有公式,使用默认粘贴行为}// 创建一个新的事务const tr = view.state.tr;const pos = view.state.selection.from;// 保存所有匹配的位置和内容const allMatches = [...doubleMatches, ...singleMatches].map(match => ({start: match.index!,end: match.index! + match[0].length,latex: match[1],fullMatch: match[0]})).sort((a, b) => a.start - b.start);// 处理文本,将公式替换为节点let lastPos = 0;let insertPos = pos;allMatches.forEach(match => {// 插入公式前的文本if (match.start > lastPos) {const textBefore = content.slice(lastPos, match.start);tr.insert(insertPos, view.state.schema.text(textBefore));insertPos += textBefore.length;}// 插入公式节点const node = this.type.create({ latex: match.latex });tr.insert(insertPos, node);insertPos += 1; // 公式节点长度为 1lastPos = match.end;});// 插入剩余的文本if (lastPos < content.length) {const remainingText = content.slice(lastPos);tr.insert(insertPos, view.state.schema.text(remainingText));}// 应用事务view.dispatch(tr);return true;},},}),];
},
3、修改mathquill的源码,阻止报错
在编辑框中进行输入的时候,会报错:
并且会阻碍后面的代码的执行,我们在代码中修改一下,不让它跑错,输出一下错误就行了,这个错误不影响编辑,只在开发环境会报错
src/utils/mathquill/mathquill.js
function pray(message, cond) {if (!cond)console.error('prayer failed: ' + message);// throw new Error('prayer failed: ' + message);
}
4、抽离公共方法
输入规则和粘贴规则的处理函数中的代码基本上都是相同的,可以进行抽离
src/extensions/formula-editor.ts
const handleRule = (state: EditorState, range: Range, match: RegExpMatchArray, type: NodeType) => {const latex = match[1];if (!latex) return null;const { tr } = state;const start = range.from;const end = range.to;tr.delete(start, end);tr.insert(start, type.create({latex}));
};
addInputRules() {return [// 双$规则{find: DOUBLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {handleRule(state, range, match, this.type);},},// 单$规则{find: SINGLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {handleRule(state, range, match, this.type);},},];
},addPasteRules() {return [{find: DOUBLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {handleRule(state, range, match, this.type);},},{find: SINGLE_DOLLAR_REGEX,handler: ({ state, range, match }) => {handleRule(state, range, match, this.type);},},];
},
七、监听内容修改
虽说上面我们在用户输入内容、粘贴内容的时候都进行了公式的转换,但是如果用户调用 setContent
之类的API修改编辑器内容的话,也需要转换公式。
addProseMirrorPlugins() {return [// 新增的内容监听插件new Plugin({key: new PluginKey('formulaEditorContentListener'),appendTransaction: (transactions, oldState, newState) => {// 检查是否有内容变化if (!transactions.some(tr => tr.docChanged)) return;const tr = newState.tr;let hasChanges = false;// 遍历所有文本节点newState.doc.descendants((node, pos) => {if (node.isText) {let text = node.text || '';let lastPos = 0;let newText = text;// 查找所有公式const doubleMatches = Array.from(text.matchAll(DOUBLE_DOLLAR_REGEX));const singleMatches = Array.from(text.matchAll(SINGLE_DOLLAR_REGEX));const allMatches = [...doubleMatches, ...singleMatches].map(match => ({start: match.index!,end: match.index! + match[0].length,latex: match[1],fullMatch: match[0]})).sort((a, b) => a.start - b.start);// 如果找到公式,处理每个公式if (allMatches.length > 0) {hasChanges = true;// 从后向前替换,以保持位置的正确性for (let i = allMatches.length - 1; i >= 0; i--) {const match = allMatches[i];const from = pos + match.start;const to = pos + match.end;// 删除原文本tr.delete(from, to);// 插入公式节点const formulaNode = this.type.create({ latex: match.latex });tr.insert(from, formulaNode);}}}return true;});return hasChanges ? tr : null;}})];
},
其实有了这个方法就一步到位了,上面的输入、粘贴都不用监听了哈哈哈
总结:公式编辑器非常的复杂,细节还是挺多的。但是这个开源的MathQuill真的挺好用,体验感很好,集成之后也没有太多奇奇怪怪的报错。另外公式编辑也非常的必要,大家学起来!另外,这么复杂的代码怎么可能都是我自己写的?90%是Cursor写的。用习惯了AI好怕自己变笨🥹🥹🥹。但是正因为有AI,我们可以更高效的完成工作,然后去学更重要的东西!自己感兴趣的东西!