【element-tiptap】添加公式编辑器【MathQuill】

前言:前面的文章【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&rsquo;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 属性规定,返回一个对象数组,对象中包含 findhandlerfind指明要匹配的正则表达式,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,我们可以更高效的完成工作,然后去学更重要的东西!自己感兴趣的东西!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/483275.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot源码-Spring Boot启动时控制台为何会打印logo以及自定义banner.txt文件控制台打印

1.当我们启动一个SpringBoot项目的时候&#xff0c;入口程序就是main方法&#xff0c;而在main方法中就执行了一个run方法。 SpringBootApplication public class StartApp {public static void main(String[] args) {// testSpringApplication.run(StartApp.class);} }publi…

【人工智能-基础】SVM中的核函数到底是什么

文章目录 支持向量机(SVM)中的核函数详解1. 什么是核函数?核函数的作用:2. 核技巧:从低维到高维的映射3. 常见的核函数类型3.1 线性核函数3.2 多项式核函数3.3 高斯径向基函数(RBF核)4. 总结支持向量机(SVM)中的核函数详解 支持向量机(SVM,Support Vector Machine)…

【Docker】Docker 容器日志过大导致磁盘爆满

docker容器的日志文件目录位于/var/lib/docker/containers/容器/容器-json.log 查看日志大小 cd /var/lib/docker/containers/ du -h --max-depth1 临时删一点 cd xxxxxxx/ tail -100 xxxxxxx-json.log > xxxxxxx-json.log 如图 解决方式&#xff08;全局&#xff09; …

SpringBoot集成Milvus|(实现向量的存储和查询)

此文章为转载文章: 原文链接 文章目录 SpringBoot集成Milvus|&#xff08;实现向量的存储和查询&#xff09;前言一、Milvus介绍二、Milvus数据库安装 1.Milvus安装环境准备&#xff08;centos7&#xff09;2.Milvus客户端安装3.attu新建Milvus集合 三、Milvus集成 1.依赖引入2…

数据结构__01

六.图 一.定义 1.有向图 2.无向图 3.完全图 4.子图 5.度 6.路径以及长度 7.简单路径 回路 简单回路 二.图的存储结构 1.邻接矩阵 有向图 无向图 有向网 2.邻接矩阵的优缺点 &#xff08;行出列入&#xff09; 3.邻接表 三.图的遍历 深度优先遍历 广度优先遍历 四.图的应用 …

嵌入式蓝桥杯学习1 电量LED

cubemx配置 1.新建一个STM32G431RBT6文件 2.在System-Core中点击SYS&#xff0c;找到Debug&#xff08;设置为Serial Wire&#xff09; 3.在System-Core中点击RCC&#xff0c;找到High Speed Clock(设置为Crystal/Ceramic Resonator) 4.打开Clock Configuration &#xff0…

【网络】协议与网络传输

目录 一、协议 1.1 认识协议 1.2 协议分层 二、OSI七层模型 三、TCP/IP五(四)层模型 四、网络传输 4.1 数据包封装与分用 4.2 网络传输基本流程 4.2.1 以太网通信 &#xff08;1&#xff09;原理 &#xff08;2&#xff09;数据碰撞问题 &#xff08;3&#xff09;…

【力扣】3274. 检查棋盘方格颜色是否相同

一、题目 给你两个字符串 coordinate1 和 coordinate2&#xff0c;代表 8 x 8 国际象棋棋盘上的两个方格的坐标。以下是棋盘格的参考图&#xff1a; 如果这两个方格颜色相同&#xff0c;返回 true&#xff0c;否则返回 false。坐标总是表示有效的棋盘方格。坐标的格式总是先字…

JavaWeb12

登陆拦截 会话技术 会话&#xff1a;用户打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开连接&#xff0c;会话结束。在一次会话中可以包含多次请求和响应 会话跟踪&#xff1a;一种维护浏览器状态的方法&#xff0c;服务器需要识…

使用STM32CubeMX配置串口各种功能

使用STM32CubeMX配置串口各种功能 STM32CubeMX软件的安装接收空闲中断STM32CubeMX配置1.新建工程2. 选择芯片3. 选择时钟和下载方式4. 配置串口5.设置工程消息6.生成代码7.修改生成的代码 空闲中断DMA转运STM32CubeMX配置4.配置串口5.设置工程消息6.生成代码7.修改生成的代码 S…

Linux详解:文件权限

文章目录 前言Linux文件权限基础文件成员与三组权限字符 权限的修改修改文件所有者总结 前言 在浩瀚的操作系统世界中&#xff0c;Linux以其开源、灵活和强大的特性&#xff0c;成为了服务器、开发环境以及众多个人用户的首选。而在Linux的众多特性中&#xff0c;文件权限机制…

openEuler 22.03 使用cephadm安装部署ceph集群

目录 目的步骤规格步骤ceph部署前准备工作安装部署ceph集群ceph集群添加node与osdceph集群一些操作组件服务操作集群进程操作 目的 使用ceph官网的cephadm无法正常安装&#xff0c;会报错ERROR: Distro openeuler version 22.03 not supported 在openEuler上实现以cephadm安装部…

xiaolin coding 图解 MySQL笔记——事务篇

1. 事务隔离级别是怎么实现的&#xff1f; 数据库中的**事务&#xff08;Transaction&#xff09;**先开启&#xff0c;然后等所有数据库操作执行完成后&#xff0c;才提交事务&#xff0c;对于已经提交的事务来说&#xff0c;该事务对数据库所做的修改将永久生效&#xff0c;…

掌握 Spring Boot 中的缓存:技术和最佳实践

缓存是一种用于将经常访问的数据临时存储在更快的存储层&#xff08;通常在内存中&#xff09;中的技术&#xff0c;以便可以更快地满足未来对该数据的请求&#xff0c;从而提高应用程序的性能和效率。在 Spring Boot 中&#xff0c;缓存是一种简单而强大的方法&#xff0c;可以…

408——数据结构(持续更新)

文章目录 一、绪论1.1 相关概念1.2 数据结构三要素1.3 相关习题1.4 复杂度1.4.1 时间复杂度1.4.2 复杂度相关习题 二、线性表 一、绪论 1.1 相关概念 数据&#xff1a;数据是信息的载体&#xff0c;所有能被输入到计算机中&#xff0c;且能被计算机处理的符号的集合。如图片、…

深入浅出:开发者如何快速上手Web3生态系统

Web3作为互联网的未来发展方向&#xff0c;正在逐步改变传统互联网架构&#xff0c;推动去中心化技术的发展。对于开发者而言&#xff0c;Web3代表着一个充满机遇与挑战的新领域&#xff0c;学习和掌握Web3的基本技术和工具&#xff0c;将为未来的项目开发提供强大的支持。那么…

C++学习日记---第16天

笔记复习 1.C对象模型 在C中&#xff0c;类内的成员变量和成员函数分开存储 我们知道&#xff0c;C中的成员变量和成员函数均可分为两种&#xff0c;一种是普通的&#xff0c;一种是静态的&#xff0c;对于静态成员变量和静态成员函数&#xff0c;我们知道他们不属于类的对象…

Leetcode 每日一题 205.同构字符串

目录 问题描述 过题图片 示例 解决方案 代码实现 题目链接 总结 问题描述 给定两个字符串 s 和 t&#xff0c;判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t&#xff0c;那么这两个字符串是同构的。具体来说&#xff0c;每个出现的字符都应当映射…

C# 集合(Collection)

文章目录 前言一、动态数组&#xff08;ArrayList&#xff09;二、哈希表&#xff08;Hashtable&#xff09;三、排序列表&#xff08;SortedList&#xff09;四、堆栈&#xff08;Stack&#xff09;五、队列&#xff08;Queue&#xff09;六、点阵列&#xff08;BitArray&…

2.5 特征降维(机器学习)

2.5 特征降维 2.5.1 降维 降维&#xff1a;是指在某些限定条件下&#xff0c;降低随机变量&#xff08;特征&#xff09;个数&#xff0c;得到一组“不相关”主变量的过程。 ndarray 维数 嵌套的层数 0维 标量 1维 向量 2维 矩阵 3维 n维 二维数组 降低的维度…