diff --git a/.changeset/tame-mice-marry.md b/.changeset/tame-mice-marry.md
new file mode 100644
index 000000000..9af71ce18
--- /dev/null
+++ b/.changeset/tame-mice-marry.md
@@ -0,0 +1,5 @@
+---
+'@wangeditor-next/plugin-formula': patch
+---
+
+feat: add formula plugin
diff --git a/packages/plugin-formula/CHANGELOG.md b/packages/plugin-formula/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/plugin-formula/package.json b/packages/plugin-formula/package.json
new file mode 100644
index 000000000..2ce873c95
--- /dev/null
+++ b/packages/plugin-formula/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@wangeditor-next/plugin-formula",
+ "version": "0.0.0",
+ "description": "wangEditor next formula 公式",
+ "author": "cycleccc <2991205548@qq.com>",
+ "type": "module",
+ "homepage": "https://github.com/cycleccc/wangEditor-next#readme",
+ "license": "MIT",
+ "types": "dist/plugin-formula/src/index.d.ts",
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "exports": {
+ ".": {
+ "types": "./dist/plugin-formula/src/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ },
+ "./dist/css/style.css": "./dist/css/style.css"
+ },
+ "directories": {
+ "lib": "dist"
+ },
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/cycleccc/wangEditor-next.git"
+ },
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development rollup -c rollup.config.js",
+ "dev-watch": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
+ "build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
+ "dev-size-stats": "cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js",
+ "size-stats": "cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js"
+ },
+ "bugs": {
+ "url": "https://github.com/cycleccc/wangEditor/issues"
+ },
+ "devDependencies": {
+ "katex": "^0.15.2",
+ "rollup-plugin-string": "^3.0.0"
+ },
+ "peerDependencies": {
+ "@wangeditor-next/editor": "5.6.10",
+ "katex": "^0.15.2",
+ "snabbdom": "^3.1.0"
+ },
+ "dependencies": {
+ "dom7": "^3.0.0",
+ "nanoid": "^3.2.0"
+ }
+}
diff --git a/packages/plugin-formula/rollup.config.js b/packages/plugin-formula/rollup.config.js
new file mode 100644
index 000000000..55c7e55f7
--- /dev/null
+++ b/packages/plugin-formula/rollup.config.js
@@ -0,0 +1,37 @@
+import { createRollupConfig } from '@wangeditor-next-shared/rollup-config'
+import { string } from 'rollup-plugin-string'
+
+import pkg from './package.json' assert { type: 'json' }
+
+const name = 'WangEditorFormulaPlugin'
+
+const configList = []
+
+// esm
+const esmConf = createRollupConfig({
+ output: {
+ file: pkg.module,
+ format: 'esm',
+ name,
+ },
+})
+
+configList.push(esmConf)
+
+// umd
+const umdConf = createRollupConfig({
+ output: {
+ file: pkg.main,
+ format: 'umd',
+ name,
+ },
+ plugins: [
+ string({
+ include: '**/*.css',
+ }),
+ ],
+})
+
+configList.push(umdConf)
+
+export default configList
diff --git a/packages/plugin-formula/src/constants/icon-svg.ts b/packages/plugin-formula/src/constants/icon-svg.ts
new file mode 100644
index 000000000..45cbd1923
--- /dev/null
+++ b/packages/plugin-formula/src/constants/icon-svg.ts
@@ -0,0 +1,16 @@
+/**
+ * @description icon svg
+ * @author wangfupeng
+ */
+
+/**
+ * 【注意】svg 字符串的长度 ,否则会导致代码体积过大
+ * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293
+ * 找不到再从 iconfont.com 搜索
+ */
+
+// 公式
+export const SIGMA_SVG = ''
+
+// 编辑
+export const PENCIL_SVG = ''
diff --git a/packages/plugin-formula/src/index.ts b/packages/plugin-formula/src/index.ts
new file mode 100644
index 000000000..bbb34233b
--- /dev/null
+++ b/packages/plugin-formula/src/index.ts
@@ -0,0 +1,11 @@
+/**
+ * @description src entry
+ * @author wangfupeng
+ */
+
+// 全局注册自定义组件,用于渲染公式
+import './register-custom-elem'
+
+import module from './module/index'
+
+export default module
diff --git a/packages/plugin-formula/src/module/custom-types.ts b/packages/plugin-formula/src/module/custom-types.ts
new file mode 100644
index 000000000..4986110d1
--- /dev/null
+++ b/packages/plugin-formula/src/module/custom-types.ts
@@ -0,0 +1,14 @@
+/**
+ * @description formula element
+ * @author wangfupeng
+ */
+
+type EmptyText = {
+ text: ''
+}
+
+export type FormulaElement = {
+ type: 'formula'
+ value: string
+ children: EmptyText[]
+}
diff --git a/packages/plugin-formula/src/module/elem-to-html.ts b/packages/plugin-formula/src/module/elem-to-html.ts
new file mode 100644
index 000000000..62ca81388
--- /dev/null
+++ b/packages/plugin-formula/src/module/elem-to-html.ts
@@ -0,0 +1,23 @@
+/**
+ * @description elem to html
+ * @author wangfupeng
+ */
+
+import { SlateElement } from '@wangeditor-next/editor'
+
+import { FormulaElement } from './custom-types'
+
+// 生成 html 的函数
+function formulaToHtml(elem: SlateElement, _childrenHtml: string): string {
+ const { value = '' } = elem as FormulaElement
+
+ return ``
+}
+
+// 配置
+const conf = {
+ type: 'formula', // 节点 type ,重要!!!
+ elemToHtml: formulaToHtml,
+}
+
+export default conf
diff --git a/packages/plugin-formula/src/module/index.ts b/packages/plugin-formula/src/module/index.ts
new file mode 100644
index 000000000..14250a120
--- /dev/null
+++ b/packages/plugin-formula/src/module/index.ts
@@ -0,0 +1,24 @@
+/**
+ * @description formula module entry
+ * @author wangfupeng
+ */
+
+import './local' // 多语言
+
+import { IModuleConf } from '@wangeditor-next/editor'
+
+import elemToHtmlConf from './elem-to-html'
+import { editFormulaMenuConf, insertFormulaMenuConf } from './menu/index'
+import parseHtmlConf from './parse-elem-html'
+import withFormula from './plugin'
+import renderElemConf from './render-elem'
+
+const module: Partial = {
+ editorPlugin: withFormula,
+ renderElems: [renderElemConf],
+ elemsToHtml: [elemToHtmlConf],
+ parseElemsHtml: [parseHtmlConf],
+ menus: [insertFormulaMenuConf, editFormulaMenuConf],
+}
+
+export default module
diff --git a/packages/plugin-formula/src/module/local.ts b/packages/plugin-formula/src/module/local.ts
new file mode 100644
index 000000000..4a64ea126
--- /dev/null
+++ b/packages/plugin-formula/src/module/local.ts
@@ -0,0 +1,26 @@
+/**
+ * @description 多语言
+ * @author wangfupeng
+ */
+
+import { i18nAddResources } from '@wangeditor-next/editor'
+
+i18nAddResources('en', {
+ formula: {
+ formula: 'Formula',
+ placeholder: 'Use LateX syntax',
+ insert: 'Insert formula',
+ edit: 'Edit formula',
+ ok: 'OK',
+ },
+})
+
+i18nAddResources('zh-CN', {
+ formula: {
+ formula: '公式',
+ placeholder: '使用 LateX 语法',
+ insert: '插入公式',
+ edit: '编辑公式',
+ ok: '确定',
+ },
+})
diff --git a/packages/plugin-formula/src/module/menu/EditFormula.ts b/packages/plugin-formula/src/module/menu/EditFormula.ts
new file mode 100644
index 000000000..153deb110
--- /dev/null
+++ b/packages/plugin-formula/src/module/menu/EditFormula.ts
@@ -0,0 +1,164 @@
+/**
+ * @description edit formula menu
+ * @author wangfupeng
+ */
+
+import {
+ DomEditor,
+ genModalButtonElems,
+ genModalTextareaElems,
+ IDomEditor,
+ IModalMenu,
+ SlateNode,
+ SlateRange,
+ SlateTransforms,
+ t,
+} from '@wangeditor-next/editor'
+
+import { PENCIL_SVG } from '../../constants/icon-svg'
+import $, { Dom7Array, DOMElement } from '../../utils/dom'
+import { genRandomStr } from '../../utils/util'
+import { FormulaElement } from '../custom-types'
+
+/**
+ * 生成唯一的 DOM ID
+ */
+function genDomID(): string {
+ return genRandomStr('w-e-insert-formula')
+}
+
+class EditFormulaMenu implements IModalMenu {
+ readonly title = t('formula.edit')
+
+ readonly iconSvg = PENCIL_SVG
+
+ readonly tag = 'button'
+
+ readonly showModal = true // 点击 button 时显示 modal
+
+ readonly modalWidth = 300
+
+ private $content: Dom7Array | null = null
+
+ private readonly textareaId = genDomID()
+
+ private readonly buttonId = genDomID()
+
+ private getSelectedElem(editor: IDomEditor): FormulaElement | null {
+ const node = DomEditor.getSelectedNodeByType(editor, 'formula')
+
+ if (node == null) { return null }
+ return node as FormulaElement
+ }
+
+ /**
+ * 获取公式 value
+ * @param editor editor
+ */
+ getValue(editor: IDomEditor): string | boolean {
+ const formulaElem = this.getSelectedElem(editor)
+
+ if (formulaElem) {
+ return formulaElem.value || ''
+ }
+ return ''
+ }
+
+ isActive(_editor: IDomEditor): boolean {
+ // 无需 active
+ return false
+ }
+
+ exec(_editor: IDomEditor, _value: string | boolean) {
+ // 点击菜单时,弹出 modal 之前,不需要执行其他代码
+ // 此处空着即可
+ }
+
+ isDisabled(editor: IDomEditor): boolean {
+ const { selection } = editor
+
+ if (selection == null) { return true }
+ if (SlateRange.isExpanded(selection)) { return true } // 选区非折叠,禁用
+
+ // 未匹配到 formula node 则禁用
+ const formulaElem = this.getSelectedElem(editor)
+
+ if (formulaElem == null) { return true }
+
+ return false
+ }
+
+ // modal 定位
+ getModalPositionNode(editor: IDomEditor): SlateNode | null {
+ return this.getSelectedElem(editor)
+ }
+
+ getModalContentElem(editor: IDomEditor): DOMElement {
+ const { textareaId, buttonId } = this
+
+ const [textareaContainerElem, textareaElem] = genModalTextareaElems(
+ t('formula.formula'),
+ textareaId,
+ t('formula.placeholder'),
+ )
+ const $textarea = $(textareaElem)
+ const [buttonContainerElem] = genModalButtonElems(buttonId, t('formula.ok'))
+
+ if (this.$content == null) {
+ // 第一次渲染
+ const $content = $('')
+
+ // 绑定事件(第一次渲染时绑定,不要重复绑定)
+ $content.on('click', `#${buttonId}`, e => {
+ e.preventDefault()
+ const value = $content.find(`#${textareaId}`).val().trim()
+
+ this.updateFormula(editor, value)
+ editor.hidePanelOrModal() // 隐藏 modal
+ })
+
+ // 记录属性,重要
+ this.$content = $content
+ }
+
+ const $content = this.$content
+
+ $content.html('') // 先清空内容
+
+ // append textarea and button
+ $content.append(textareaContainerElem)
+ $content.append(buttonContainerElem)
+
+ // 设置 input val
+ const value = this.getValue(editor)
+
+ $textarea.val(value)
+
+ // focus 一个 input(异步,此时 DOM 尚未渲染)
+ setTimeout(() => {
+ $textarea.focus()
+ })
+
+ return $content[0]
+ }
+
+ private updateFormula(editor: IDomEditor, value: string) {
+ if (!value) { return }
+
+ // 还原选区
+ editor.restoreSelection()
+
+ if (this.isDisabled(editor)) { return }
+
+ const selectedElem = this.getSelectedElem(editor)
+
+ if (selectedElem == null) { return }
+
+ const path = DomEditor.findPath(editor, selectedElem)
+ const props: Partial = { value }
+
+ SlateTransforms.setNodes(editor, props, { at: path })
+ }
+}
+
+export default EditFormulaMenu
diff --git a/packages/plugin-formula/src/module/menu/InsertFormula.ts b/packages/plugin-formula/src/module/menu/InsertFormula.ts
new file mode 100644
index 000000000..a1ca82278
--- /dev/null
+++ b/packages/plugin-formula/src/module/menu/InsertFormula.ts
@@ -0,0 +1,149 @@
+/**
+ * @description insert formula menu
+ * @author wangfupeng
+ */
+
+import {
+ DomEditor,
+ genModalButtonElems,
+ genModalTextareaElems,
+ IDomEditor,
+ IModalMenu,
+ SlateNode,
+ SlateRange,
+ t,
+} from '@wangeditor-next/editor'
+
+import { SIGMA_SVG } from '../../constants/icon-svg'
+import $, { Dom7Array, DOMElement } from '../../utils/dom'
+import { genRandomStr } from '../../utils/util'
+import { FormulaElement } from '../custom-types'
+
+/**
+ * 生成唯一的 DOM ID
+ */
+function genDomID(): string {
+ return genRandomStr('w-e-insert-formula')
+}
+
+class InsertFormulaMenu implements IModalMenu {
+ readonly title = t('formula.insert')
+
+ readonly iconSvg = SIGMA_SVG
+
+ readonly tag = 'button'
+
+ readonly showModal = true // 点击 button 时显示 modal
+
+ readonly modalWidth = 300
+
+ private $content: Dom7Array | null = null
+
+ private readonly textareaId = genDomID()
+
+ private readonly buttonId = genDomID()
+
+ getValue(_editor: IDomEditor): string | boolean {
+ // 插入菜单,不需要 value
+ return ''
+ }
+
+ isActive(_editor: IDomEditor): boolean {
+ // 任何时候,都不用激活 menu
+ return false
+ }
+
+ exec(_editor: IDomEditor, _value: string | boolean) {
+ // 点击菜单时,弹出 modal 之前,不需要执行其他代码
+ // 此处空着即可
+ }
+
+ isDisabled(editor: IDomEditor): boolean {
+ const { selection } = editor
+
+ if (selection == null) { return true }
+ if (SlateRange.isExpanded(selection)) { return true } // 选区非折叠,禁用
+
+ const selectedElems = DomEditor.getSelectedElems(editor)
+
+ const hasVoidElem = selectedElems.some(elem => editor.isVoid(elem))
+
+ if (hasVoidElem) { return true } // 选中了 void 元素,禁用
+
+ const hasPreElem = selectedElems.some(elem => DomEditor.getNodeType(elem) === 'pre')
+
+ if (hasPreElem) { return true } // 选中了 pre 原则,禁用
+
+ return false
+ }
+
+ getModalPositionNode(_editor: IDomEditor): SlateNode | null {
+ return null // modal 依据选区定位
+ }
+
+ getModalContentElem(editor: IDomEditor): DOMElement {
+ const { textareaId, buttonId } = this
+
+ const [textareaContainerElem, textareaElem] = genModalTextareaElems(
+ t('formula.formula'),
+ textareaId,
+ t('formula.placeholder'),
+ )
+ const $textarea = $(textareaElem)
+ const [buttonContainerElem] = genModalButtonElems(buttonId, t('formula.ok'))
+
+ if (this.$content == null) {
+ // 第一次渲染
+ const $content = $('')
+
+ // 绑定事件(第一次渲染时绑定,不要重复绑定)
+ $content.on('click', `#${buttonId}`, e => {
+ e.preventDefault()
+ const value = $content.find(`#${textareaId}`).val().trim()
+
+ this.insertFormula(editor, value)
+ editor.hidePanelOrModal() // 隐藏 modal
+ })
+
+ // 记录属性,重要
+ this.$content = $content
+ }
+
+ const $content = this.$content
+
+ $content.html('') // 先清空内容
+
+ // append textarea and button
+ $content.append(textareaContainerElem)
+ $content.append(buttonContainerElem)
+
+ // 设置 input val
+ $textarea.val('')
+
+ // focus 一个 input(异步,此时 DOM 尚未渲染)
+ setTimeout(() => {
+ $textarea.focus()
+ })
+
+ return $content[0]
+ }
+
+ private insertFormula(editor: IDomEditor, value: string) {
+ if (!value) { return }
+
+ // 还原选区
+ editor.restoreSelection()
+
+ if (this.isDisabled(editor)) { return }
+
+ const formulaElem: FormulaElement = {
+ type: 'formula',
+ value,
+ children: [{ text: '' }], // void node 需要有一个空 text
+ }
+
+ editor.insertNode(formulaElem)
+ }
+}
+
+export default InsertFormulaMenu
diff --git a/packages/plugin-formula/src/module/menu/index.ts b/packages/plugin-formula/src/module/menu/index.ts
new file mode 100644
index 000000000..1bb9594f6
--- /dev/null
+++ b/packages/plugin-formula/src/module/menu/index.ts
@@ -0,0 +1,21 @@
+/**
+ * @description formula menu entry
+ * @author wangfupeng
+ */
+
+import EditFormulaMenu from './EditFormula'
+import InsertFormulaMenu from './InsertFormula'
+
+export const insertFormulaMenuConf = {
+ key: 'insertFormula', // menu key ,唯一。注册之后,可配置到工具栏
+ factory() {
+ return new InsertFormulaMenu()
+ },
+}
+
+export const editFormulaMenuConf = {
+ key: 'editFormula', // menu key ,唯一。注册之后,可配置到工具栏
+ factory() {
+ return new EditFormulaMenu()
+ },
+}
diff --git a/packages/plugin-formula/src/module/parse-elem-html.ts b/packages/plugin-formula/src/module/parse-elem-html.ts
new file mode 100644
index 000000000..b428f7441
--- /dev/null
+++ b/packages/plugin-formula/src/module/parse-elem-html.ts
@@ -0,0 +1,30 @@
+/**
+ * @description parse elem html
+ * @author wangfupeng
+ */
+
+import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor-next/editor'
+
+import { DOMElement } from '../utils/dom'
+import { FormulaElement } from './custom-types'
+
+function parseHtml(
+ elem: DOMElement,
+ _children: SlateDescendant[],
+ _editor: IDomEditor,
+): SlateElement {
+ const value = elem.getAttribute('data-value') || ''
+
+ return {
+ type: 'formula',
+ value,
+ children: [{ text: '' }], // void node 必须有一个空白 text
+ } as FormulaElement
+}
+
+const parseHtmlConf = {
+ selector: 'span[data-w-e-type="formula"]',
+ parseElemHtml: parseHtml,
+}
+
+export default parseHtmlConf
diff --git a/packages/plugin-formula/src/module/plugin.ts b/packages/plugin-formula/src/module/plugin.ts
new file mode 100644
index 000000000..c1185d9ac
--- /dev/null
+++ b/packages/plugin-formula/src/module/plugin.ts
@@ -0,0 +1,37 @@
+/**
+ * @description formula plugin
+ * @author wangfupeng
+ */
+
+import { DomEditor, IDomEditor } from '@wangeditor-next/editor'
+
+function withFormula(editor: T) {
+ const { isInline, isVoid } = editor
+ const newEditor = editor
+
+ // 重写 isInline
+ newEditor.isInline = elem => {
+ const type = DomEditor.getNodeType(elem)
+
+ if (type === 'formula') {
+ return true
+ }
+
+ return isInline(elem)
+ }
+
+ // 重写 isVoid
+ newEditor.isVoid = elem => {
+ const type = DomEditor.getNodeType(elem)
+
+ if (type === 'formula') {
+ return true
+ }
+
+ return isVoid(elem)
+ }
+
+ return newEditor
+}
+
+export default withFormula
diff --git a/packages/plugin-formula/src/module/render-elem.ts b/packages/plugin-formula/src/module/render-elem.ts
new file mode 100644
index 000000000..5abebf1c3
--- /dev/null
+++ b/packages/plugin-formula/src/module/render-elem.ts
@@ -0,0 +1,54 @@
+/**
+ * @description render elem
+ * @author wangfupeng
+ */
+
+import { DomEditor, IDomEditor, SlateElement } from '@wangeditor-next/editor'
+import { h, VNode } from 'snabbdom'
+
+import { FormulaElement } from './custom-types'
+
+function renderFormula(elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {
+ // 当前节点是否选中
+ const selected = DomEditor.isNodeSelected(editor, elem)
+
+ // 构建 formula vnode
+ const { value = '' } = elem as FormulaElement
+ const formulaVnode = h(
+ 'w-e-formula-card',
+ {
+ dataset: { value },
+ },
+ null,
+ )
+
+ // 构建容器 vnode
+ const containerVnode = h(
+ 'div',
+ {
+ props: {
+ contentEditable: false, // 不可编辑
+ },
+ style: {
+ display: 'inline-block', // inline
+ marginLeft: '3px',
+ marginRight: '3px',
+ border: selected // 选中/不选中,样式不一样
+ ? '2px solid var(--w-e-textarea-selected-border-color)' // wangEditor 提供了 css var https://www.wangeditor.com/v5/theme.html
+ : '2px solid transparent',
+ borderRadius: '3px',
+ padding: '3px 3px',
+ },
+ },
+ [formulaVnode],
+ )
+
+ return containerVnode
+}
+
+const conf = {
+ type: 'formula', // 节点 type ,重要!!!
+ renderElem: renderFormula,
+}
+
+export default conf
diff --git a/packages/plugin-formula/src/register-custom-elem/README.md b/packages/plugin-formula/src/register-custom-elem/README.md
new file mode 100644
index 000000000..f09936e9a
--- /dev/null
+++ b/packages/plugin-formula/src/register-custom-elem/README.md
@@ -0,0 +1,3 @@
+# register custom elem
+
+全局注册一个自定义元素 `` 用于渲染公式。
diff --git a/packages/plugin-formula/src/register-custom-elem/index.ts b/packages/plugin-formula/src/register-custom-elem/index.ts
new file mode 100644
index 000000000..9b32edf50
--- /dev/null
+++ b/packages/plugin-formula/src/register-custom-elem/index.ts
@@ -0,0 +1,67 @@
+/**
+ * @description 注册自定义 elem
+ * @author wangfupeng
+ */
+
+import './native-shim'
+
+import katex from 'katex'
+// @ts-ignore
+import katexStyleContent from 'katex/dist/katex.css'
+
+console.log(katexStyleContent) // CSS 内容作为字符串
+
+class WangEditorFormulaCard extends HTMLElement {
+ private span: HTMLElement
+
+ // 监听的 attr
+ static get observedAttributes() {
+ return ['data-value']
+ }
+
+ constructor() {
+ super()
+ const shadow = this.attachShadow({ mode: 'open' })
+ const document = shadow.ownerDocument
+
+ const style = document.createElement('style')
+
+ style.innerHTML = katexStyleContent // 加载 css 文本
+ shadow.appendChild(style)
+
+ const span = document.createElement('span')
+
+ span.style.display = 'inline-block'
+ shadow.appendChild(span)
+ this.span = span
+ }
+
+ // connectedCallback() {
+ // // 当 custom element首次被插入文档DOM时,被调用
+ // console.log('connected')
+ // }
+ // disconnectedCallback() {
+ // // 当 custom element从文档DOM中删除时,被调用
+ // console.log('disconnected')
+ // }
+ // adoptedCallback() {
+ // // 当 custom element被移动到新的文档时,被调用
+ // console.log('adopted')
+ // }
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
+ if (name === 'data-value') {
+ if (oldValue === newValue) { return }
+ this.render(newValue || '')
+ }
+ }
+
+ private render(value: string) {
+ katex.render(value, this.span, {
+ throwOnError: false,
+ })
+ }
+}
+
+if (!window.customElements.get('w-e-formula-card')) {
+ window.customElements.define('w-e-formula-card', WangEditorFormulaCard)
+}
diff --git a/packages/plugin-formula/src/register-custom-elem/native-shim.ts b/packages/plugin-formula/src/register-custom-elem/native-shim.ts
new file mode 100644
index 000000000..2807b1b7e
--- /dev/null
+++ b/packages/plugin-formula/src/register-custom-elem/native-shim.ts
@@ -0,0 +1,51 @@
+// @ts-nocheck
+
+// 参考 https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js
+
+/**
+ * @license
+ * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+ * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+ * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+ * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+ * Code distributed by Google as part of the polymer project is also
+ * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+ */
+
+/**
+ * This shim allows elements written in, or compiled to, ES5 to work on native
+ * implementations of Custom Elements v1. It sets new.target to the value of
+ * this.constructor so that the native HTMLElement constructor can access the
+ * current under-construction element's definition.
+ */
+(function () {
+ if (
+ // No Reflect, no classes, no need for shim because native custom elements
+ // require ES2015 classes or Reflect.
+ window.Reflect === undefined
+ || window.customElements === undefined
+ // The webcomponentsjs custom elements polyfill doesn't require
+ // ES2015-compatible construction (`super()` or `Reflect.construct`).
+ || window.customElements.polyfillWrapFlushCallback
+ ) {
+ return
+ }
+ const BuiltInHTMLElement = HTMLElement
+ /**
+ * With jscompiler's RECOMMENDED_FLAGS the function name will be optimized away.
+ * However, if we declare the function as a property on an object literal, and
+ * use quotes for the property name, then closure will leave that much intact,
+ * which is enough for the JS VM to correctly set Function.prototype.name.
+ */
+ const wrapperForTheName = {
+ // eslint-disable-next-line func-names
+ HTMLElement: /** @this {!Object} */ function HTMLElement() {
+ return Reflect.construct(BuiltInHTMLElement, [], /** @type {!Function} */ this.constructor)
+ },
+ }
+
+ window.HTMLElement = wrapperForTheName.HTMLElement
+ HTMLElement.prototype = BuiltInHTMLElement.prototype
+ HTMLElement.prototype.constructor = HTMLElement
+ Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement)
+}())
diff --git a/packages/plugin-formula/src/utils/dom.ts b/packages/plugin-formula/src/utils/dom.ts
new file mode 100644
index 000000000..a5a0506ea
--- /dev/null
+++ b/packages/plugin-formula/src/utils/dom.ts
@@ -0,0 +1,34 @@
+/**
+ * @description DOM 操作
+ * @author wangfupeng
+ */
+
+import $, {
+ append, find, focus, html, is, on, parents, val,
+} from 'dom7'
+
+// COMPAT: This is required to prevent TypeScript aliases from doing some very
+// weird things for Slate's types with the same name as globals. (2019/11/27)
+// https://github.com/microsoft/TypeScript/issues/35002
+import DOMNode = globalThis.Node
+import DOMComment = globalThis.Comment
+import DOMElement = globalThis.Element
+import DOMText = globalThis.Text
+import DOMRange = globalThis.Range
+import DOMSelection = globalThis.Selection
+import DOMStaticRange = globalThis.StaticRange
+
+if (append) { $.fn.append = append }
+if (html) { $.fn.html = html }
+if (val) { $.fn.val = val }
+if (on) { $.fn.on = on }
+if (focus) { $.fn.focus = focus }
+if (is) { $.fn.is = is }
+if (parents) { $.fn.parents = parents }
+if (find) { $.fn.find = find }
+
+export { Dom7Array } from 'dom7'
+export default $
+export {
+ DOMComment, DOMElement, DOMNode, DOMRange, DOMSelection, DOMStaticRange, DOMText,
+}
diff --git a/packages/plugin-formula/src/utils/util.ts b/packages/plugin-formula/src/utils/util.ts
new file mode 100644
index 000000000..7c34a2d86
--- /dev/null
+++ b/packages/plugin-formula/src/utils/util.ts
@@ -0,0 +1,19 @@
+/**
+ * @description 工具函数
+ * @author wangfupeng
+ */
+
+import { nanoid } from 'nanoid'
+
+/**
+ * 获取随机数字符串
+ * @param prefix 前缀
+ * @returns 随机数字符串
+ */
+export function genRandomStr(prefix: string = 'r'): string {
+ return `${prefix}-${nanoid()}`
+}
+
+// export function replaceSymbols(str: string) {
+// return str.replace(//g, '>')
+// }
diff --git a/packages/plugin-formula/tsconfig.json b/packages/plugin-formula/tsconfig.json
new file mode 100644
index 000000000..9bef938c9
--- /dev/null
+++ b/packages/plugin-formula/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {},
+ "extends": "../../tsconfig.json",
+ "include": [
+ "./src/**/*",
+ "../custom-types.d.ts"
+ ]
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 8400a3a82..e513558ff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3526,6 +3526,21 @@ __metadata:
languageName: unknown
linkType: soft
+"@wangeditor-next/plugin-formula@workspace:packages/plugin-formula":
+ version: 0.0.0-use.local
+ resolution: "@wangeditor-next/plugin-formula@workspace:packages/plugin-formula"
+ dependencies:
+ dom7: "npm:^3.0.0"
+ katex: "npm:^0.15.2"
+ nanoid: "npm:^3.2.0"
+ rollup-plugin-string: "npm:^3.0.0"
+ peerDependencies:
+ "@wangeditor-next/editor": 5.6.10
+ katex: ^0.15.2
+ snabbdom: ^3.1.0
+ languageName: unknown
+ linkType: soft
+
"@wangeditor-next/plugin-markdown@workspace:packages/plugin-markdown":
version: 0.0.0-use.local
resolution: "@wangeditor-next/plugin-markdown@workspace:packages/plugin-markdown"
@@ -4685,6 +4700,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^8.0.0":
+ version: 8.3.0
+ resolution: "commander@npm:8.3.0"
+ checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
+ languageName: node
+ linkType: hard
+
"commander@npm:~12.1.0":
version: 12.1.0
resolution: "commander@npm:12.1.0"
@@ -8432,6 +8454,17 @@ __metadata:
languageName: node
linkType: hard
+"katex@npm:^0.15.2":
+ version: 0.15.6
+ resolution: "katex@npm:0.15.6"
+ dependencies:
+ commander: "npm:^8.0.0"
+ bin:
+ katex: cli.js
+ checksum: 10c0/722db3b0442138f76d76017a384acd45ae0cbe67d730f20cbbfed0bf519c3640aa5352ba19e813743a63aead6065082bd225d21c04a16f58abb1d52684f95333
+ languageName: node
+ linkType: hard
+
"klaw-sync@npm:^6.0.0":
version: 6.0.0
resolution: "klaw-sync@npm:6.0.0"
@@ -9247,7 +9280,7 @@ __metadata:
languageName: node
linkType: hard
-"nanoid@npm:^3.3.7":
+"nanoid@npm:^3.2.0, nanoid@npm:^3.3.7":
version: 3.3.7
resolution: "nanoid@npm:3.3.7"
bin:
@@ -11253,6 +11286,15 @@ __metadata:
languageName: node
linkType: hard
+"rollup-plugin-string@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "rollup-plugin-string@npm:3.0.0"
+ dependencies:
+ rollup-pluginutils: "npm:^2.4.1"
+ checksum: 10c0/83bbc2230d5271bc014739d52320b66cd4071fc4f7181369533809e4a3f0269b6cb4873caa96426396b36f694023a9f68f5de7a94254e1f2764ca6bacff61995
+ languageName: node
+ linkType: hard
+
"rollup-plugin-typescript2@npm:^0.36.0":
version: 0.36.0
resolution: "rollup-plugin-typescript2@npm:0.36.0"
@@ -11288,7 +11330,7 @@ __metadata:
languageName: node
linkType: hard
-"rollup-pluginutils@npm:^2.8.2":
+"rollup-pluginutils@npm:^2.4.1, rollup-pluginutils@npm:^2.8.2":
version: 2.8.2
resolution: "rollup-pluginutils@npm:2.8.2"
dependencies: