react-mathtype-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/cjs/MathTypeEditor.js +480 -0
- package/dist/cjs/const.js +326 -0
- package/dist/cjs/index.js +11 -0
- package/dist/esm/MathTypeEditor.js +475 -0
- package/dist/esm/const.js +323 -0
- package/dist/esm/index.js +5 -0
- package/dist/types/MathTypeEditor.d.ts +12 -0
- package/dist/types/MathTypeEditor.d.ts.map +1 -0
- package/dist/types/const.d.ts +5 -0
- package/dist/types/const.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/src/MathTypeEditor.d.ts +12 -0
- package/dist/types/src/MathTypeEditor.d.ts.map +1 -0
- package/dist/types/src/const.d.ts +5 -0
- package/dist/types/src/const.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/main.d.ts +3 -0
- package/dist/types/src/main.d.ts.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# MathType 公式编辑器
|
|
2
|
+
|
|
3
|
+
一个独立的数学公式编辑器组件,基于 React 和 Ant Design 开发,提供两种公式编辑模式:基于 MathLive 的可视化编辑和基于 LaTeX 的文本编辑。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🎨 **双模式编辑**:支持 MathLive 可视化编辑和 LaTeX 文本编辑
|
|
8
|
+
- 🔍 **实时预览**:使用 KaTeX 进行高质量公式渲染
|
|
9
|
+
- 📚 **丰富公式库**:内置数学、希腊字母、关系符号等多种预设公式
|
|
10
|
+
- 📋 **智能粘贴**:自动处理粘贴的公式内容
|
|
11
|
+
- 📱 **响应式设计**:适配不同屏幕尺寸
|
|
12
|
+
- ⚡ **高性能**:优化的渲染和更新机制
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install react-mathtype-editor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 使用方法
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
import React, { useState } from 'react';
|
|
26
|
+
import { MathEditor } from 'react-mathtype-editor';
|
|
27
|
+
import 'react-mathtype-editor/dist/style.css';
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
const [formula, setFormula] = useState('');
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<MathEditor
|
|
35
|
+
defaultValue={formula}
|
|
36
|
+
onChange={(value) => setFormula(value)}
|
|
37
|
+
style={{ width: '100%' }}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default App;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### Props
|
|
49
|
+
|
|
50
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
51
|
+
|------|------|--------|------|
|
|
52
|
+
| defaultValue | string | '' | 默认的 LaTeX 公式 |
|
|
53
|
+
| onChange | (value: string) => void | - | 公式变化时的回调函数 |
|
|
54
|
+
| style | React.CSSProperties | - | 自定义样式 |
|
|
55
|
+
|
|
56
|
+
## 功能说明
|
|
57
|
+
|
|
58
|
+
### 编辑模式
|
|
59
|
+
|
|
60
|
+
- **MathLive 模式**:可视化公式编辑器,可以直接点击输入数学符号
|
|
61
|
+
- **LaTeX 模式**:纯文本编辑,直接输入 LaTeX 代码
|
|
62
|
+
|
|
63
|
+
### 公式库
|
|
64
|
+
|
|
65
|
+
提供了多个分类的预设公式,包括:
|
|
66
|
+
- 数学符号
|
|
67
|
+
- 希腊字母
|
|
68
|
+
- 关系符号
|
|
69
|
+
- 逻辑符号
|
|
70
|
+
- 矩阵
|
|
71
|
+
- 化学方程式等
|
|
72
|
+
|
|
73
|
+
### 粘贴处理
|
|
74
|
+
|
|
75
|
+
支持智能粘贴处理,能够:
|
|
76
|
+
- 自动识别公式和文本
|
|
77
|
+
- 修复不匹配的括号
|
|
78
|
+
- 处理混合内容(公式+文本)
|
|
79
|
+
|
|
80
|
+
## 项目架构
|
|
81
|
+
|
|
82
|
+
### 技术栈
|
|
83
|
+
- **前端框架**:React 17+/18+
|
|
84
|
+
- **UI 组件库**:Ant Design 5.x
|
|
85
|
+
- **公式引擎**:MathLive(编辑) + KaTeX(渲染)
|
|
86
|
+
- **样式处理**:Less
|
|
87
|
+
- **类型检查**:TypeScript
|
|
88
|
+
|
|
89
|
+
### 组件结构
|
|
90
|
+
```
|
|
91
|
+
MathEditor (主组件)
|
|
92
|
+
├── 模式切换区 (Segmented)
|
|
93
|
+
├── 编辑内容区
|
|
94
|
+
│ ├── MathLive 模式
|
|
95
|
+
│ │ └── math-field 元素
|
|
96
|
+
│ └── LaTeX 模式
|
|
97
|
+
│ ├── 输入区域 (Input.TextArea)
|
|
98
|
+
│ └── 预览区域 (HTML 渲染)
|
|
99
|
+
└── 公式库区 (Tabs)
|
|
100
|
+
└── 预设公式 (KATEXLIST)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 数据流
|
|
104
|
+
1. 用户在任一编辑模式中输入公式
|
|
105
|
+
2. 组件状态同步更新 LaTeX 字符串
|
|
106
|
+
3. 实时渲染预览
|
|
107
|
+
4. 通过 onChange 回调通知父组件
|
|
108
|
+
|
|
109
|
+
## 开发
|
|
110
|
+
|
|
111
|
+
### 环境搭建
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 克隆项目
|
|
115
|
+
git clone <repository-url>
|
|
116
|
+
|
|
117
|
+
# 安装依赖
|
|
118
|
+
npm install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 开发模式
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# 启动开发服务器
|
|
125
|
+
npm run dev
|
|
126
|
+
|
|
127
|
+
# 运行类型检查
|
|
128
|
+
npm run type-check
|
|
129
|
+
|
|
130
|
+
# 运行 ESLint 检查
|
|
131
|
+
npm run lint
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 构建发布
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 构建生产版本
|
|
138
|
+
npm run build
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
有关更详细的技术设计文档,请参阅 [SDD.md](./SDD.md)。
|
|
142
|
+
|
|
143
|
+
## 贡献指南
|
|
144
|
+
|
|
145
|
+
### 代码贡献
|
|
146
|
+
1. Fork 项目
|
|
147
|
+
2. 创建功能分支
|
|
148
|
+
3. 提交代码
|
|
149
|
+
4. 发起 Pull Request
|
|
150
|
+
|
|
151
|
+
### 问题报告
|
|
152
|
+
- 详细描述问题现象
|
|
153
|
+
- 提供重现步骤
|
|
154
|
+
- 附上错误日志或截图
|
|
155
|
+
|
|
156
|
+
## 许可证
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const antd_1 = require("antd");
|
|
8
|
+
const katex_1 = __importDefault(require("katex"));
|
|
9
|
+
const react_1 = require("react");
|
|
10
|
+
const const_1 = require("./const");
|
|
11
|
+
require("mathlive");
|
|
12
|
+
require("katex/dist/katex.css");
|
|
13
|
+
require("../styles/mathType.less");
|
|
14
|
+
const MathEditor = ({ defaultValue, onChange, style }) => {
|
|
15
|
+
const [activeKey, setActiveKey] = (0, react_1.useState)(const_1.KATEXLIST[0]?.name || '1');
|
|
16
|
+
const [editMode, setEditMode] = (0, react_1.useState)('mathlive');
|
|
17
|
+
const [latexVal, setLatexVal] = (0, react_1.useState)(defaultValue || '');
|
|
18
|
+
const mfRef = (0, react_1.useRef)(null);
|
|
19
|
+
// Initialize value when opening or defaultValue prop changes
|
|
20
|
+
(0, react_1.useEffect)(() => {
|
|
21
|
+
const initialValue = defaultValue || '';
|
|
22
|
+
setLatexVal(initialValue);
|
|
23
|
+
// 延迟赋值,确保 DOM 已挂载
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (mfRef.current && mfRef.current.value !== initialValue) {
|
|
26
|
+
let val = initialValue.trim();
|
|
27
|
+
// 如果是 $ 开头结尾,去掉 $,方便 MathLive 识别
|
|
28
|
+
if (val.startsWith('$') && val.endsWith('$') && val.length > 2) {
|
|
29
|
+
val = val.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
mfRef.current.value = val;
|
|
32
|
+
}
|
|
33
|
+
}, 100);
|
|
34
|
+
}, [defaultValue]);
|
|
35
|
+
// Sync internal changes to parent
|
|
36
|
+
(0, react_1.useEffect)(() => {
|
|
37
|
+
if (onChange) {
|
|
38
|
+
console.log('value', latexVal);
|
|
39
|
+
onChange(latexVal);
|
|
40
|
+
}
|
|
41
|
+
}, [latexVal]);
|
|
42
|
+
// Sync latexVal to mathfield when switching to mathlive mode or when latexVal changes externally
|
|
43
|
+
// But be careful not to create a loop.
|
|
44
|
+
// We use effects to sync when switching modes primarily.
|
|
45
|
+
const handleModeChange = (key) => {
|
|
46
|
+
setEditMode(key);
|
|
47
|
+
if (key === 'mathlive') {
|
|
48
|
+
// Latex -> MathLive
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
if (mfRef.current) {
|
|
51
|
+
let val = latexVal.trim();
|
|
52
|
+
// 如果是 $ 开头结尾,去掉 $,方便 MathLive 识别
|
|
53
|
+
if (val.startsWith('$') && val.endsWith('$') && val.length > 2) {
|
|
54
|
+
val = val.slice(1, -1);
|
|
55
|
+
}
|
|
56
|
+
mfRef.current.value = val;
|
|
57
|
+
}
|
|
58
|
+
}, 0);
|
|
59
|
+
}
|
|
60
|
+
else if (mfRef.current) {
|
|
61
|
+
// MathLive -> Latex
|
|
62
|
+
setLatexVal(mfRef.current.getValue());
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// Handle MathLive input changes
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
let isMounted = true;
|
|
68
|
+
let cleanup;
|
|
69
|
+
const initListeners = () => {
|
|
70
|
+
if (!isMounted)
|
|
71
|
+
return;
|
|
72
|
+
const mf = mfRef.current;
|
|
73
|
+
if (mf) {
|
|
74
|
+
// 注入滚动条样式到 Shadow DOM
|
|
75
|
+
if (mf.shadowRoot) {
|
|
76
|
+
const styleEl = document.createElement('style');
|
|
77
|
+
styleEl.textContent = `
|
|
78
|
+
/* Webkit scrollbar styling */
|
|
79
|
+
.ML__latex {
|
|
80
|
+
width: 100% !important;
|
|
81
|
+
display:flex !important;
|
|
82
|
+
min-height: unset !important; /* 允许内容撑开高度 */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ML__base {
|
|
86
|
+
flex:1;
|
|
87
|
+
display:flex !important;
|
|
88
|
+
flex-wrap:wrap;
|
|
89
|
+
gap: 10px 0px ;
|
|
90
|
+
align-items:center;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
width: 100%;
|
|
93
|
+
}
|
|
94
|
+
::-webkit-scrollbar {
|
|
95
|
+
width: 8px;
|
|
96
|
+
height: 8px;
|
|
97
|
+
}
|
|
98
|
+
::-webkit-scrollbar-track {
|
|
99
|
+
background: #f1f1f1;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
}
|
|
102
|
+
::-webkit-scrollbar-thumb {
|
|
103
|
+
background: #ccc;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
}
|
|
106
|
+
::-webkit-scrollbar-thumb:hover {
|
|
107
|
+
background: #bbb;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Ensure the host and internal containers allow scrolling */
|
|
111
|
+
:host {
|
|
112
|
+
overflow: auto;
|
|
113
|
+
display: block;
|
|
114
|
+
max-height: 250px; /* 控制整个 math-field 的最大高度 */
|
|
115
|
+
}
|
|
116
|
+
/* MathLive 内部容器类通常是 .ML__field-container 或 .ML__content
|
|
117
|
+
我们针对这些容器设置滚动行为 */
|
|
118
|
+
.ML__field-container, .ML__content, .ML__container {
|
|
119
|
+
overflow: visible !important;
|
|
120
|
+
max-height: inherit;
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
// 避免重复添加
|
|
124
|
+
if (!mf.shadowRoot.querySelector('style[data-custom-scrollbar]')) {
|
|
125
|
+
styleEl.setAttribute('data-custom-scrollbar', 'true');
|
|
126
|
+
mf.shadowRoot.appendChild(styleEl);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const onInput = (_evt) => {
|
|
130
|
+
// Update local state to keep in sync
|
|
131
|
+
setLatexVal(mf.getValue());
|
|
132
|
+
};
|
|
133
|
+
// 辅助函数:分割包含LaTeX命令的文本,正确处理嵌套大括号
|
|
134
|
+
const splitWithLatexCommands = (text) => {
|
|
135
|
+
const regex = /\\x(?:longequal|xrightarrow|xleftarrow)\{(?:[^{}]|\{[^{}]*\})*\}/g;
|
|
136
|
+
const result = [];
|
|
137
|
+
let lastIndex = 0;
|
|
138
|
+
// 创建一个新的正则表达式实例用于执行exec
|
|
139
|
+
const execRegex = new RegExp(regex.source, 'g');
|
|
140
|
+
// 先获取第一个匹配项
|
|
141
|
+
let match = execRegex.exec(text);
|
|
142
|
+
while (match !== null) {
|
|
143
|
+
// 添加匹配前的文本
|
|
144
|
+
if (match.index > lastIndex) {
|
|
145
|
+
result.push(text.substring(lastIndex, match.index));
|
|
146
|
+
}
|
|
147
|
+
// 添加匹配的LaTeX命令
|
|
148
|
+
result.push(match[0]);
|
|
149
|
+
lastIndex = match.index + match[0].length;
|
|
150
|
+
// 获取下一个匹配项
|
|
151
|
+
match = execRegex.exec(text);
|
|
152
|
+
}
|
|
153
|
+
// 添加最后剩余的文本
|
|
154
|
+
if (lastIndex < text.length) {
|
|
155
|
+
result.push(text.substring(lastIndex));
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
};
|
|
159
|
+
const onPaste = (e) => {
|
|
160
|
+
const text = e.clipboardData?.getData('text/plain');
|
|
161
|
+
// 辅助函数:修复常见的 LaTeX 格式错误(包括大括号不匹配、环境未闭合等)
|
|
162
|
+
const fixLatex = (latex) => {
|
|
163
|
+
let current = latex;
|
|
164
|
+
// 1. 清理末尾可能的无效反斜杠(防止转义了后续补全的闭合符)
|
|
165
|
+
// 如果以奇数个反斜杠结尾,去掉最后一个
|
|
166
|
+
if (/\\+$/.test(current)) {
|
|
167
|
+
const backslashMatch = current.match(/\\+$/);
|
|
168
|
+
if (backslashMatch && backslashMatch[0].length % 2 !== 0) {
|
|
169
|
+
current = current.slice(0, -1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 2. 补全未闭合的环境 \begin{xxx} ... \end{xxx}
|
|
173
|
+
const stack = [];
|
|
174
|
+
// 简单的正则匹配 \begin{...} 和 \end{...}
|
|
175
|
+
// 注意:这里简化处理,假设环境名中不包含花括号
|
|
176
|
+
const envRegex = /\\(begin|end)\{([^}]+)\}/g;
|
|
177
|
+
let match;
|
|
178
|
+
// eslint-disable-next-line no-cond-assign
|
|
179
|
+
while ((match = envRegex.exec(current)) !== null) {
|
|
180
|
+
const type = match[1];
|
|
181
|
+
const envName = match[2];
|
|
182
|
+
if (type === 'begin') {
|
|
183
|
+
stack.push(envName);
|
|
184
|
+
}
|
|
185
|
+
else if (type === 'end') {
|
|
186
|
+
const lastEnv = stack[stack.length - 1];
|
|
187
|
+
if (lastEnv === envName) {
|
|
188
|
+
stack.pop();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 补全剩余的环境
|
|
193
|
+
while (stack.length > 0) {
|
|
194
|
+
const env = stack.pop();
|
|
195
|
+
current += `\\end{${env}}`;
|
|
196
|
+
}
|
|
197
|
+
// 3. 补全未闭合的花括号 {}
|
|
198
|
+
// 需要正确处理转义字符 \{ 和 \}
|
|
199
|
+
let braceBalance = 0;
|
|
200
|
+
let i = 0;
|
|
201
|
+
while (i < current.length) {
|
|
202
|
+
const char = current[i];
|
|
203
|
+
if (char === '\\') {
|
|
204
|
+
// 跳过转义字符(包括 \{, \}, \\ 等)
|
|
205
|
+
i += 2;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (char === '{') {
|
|
209
|
+
braceBalance++;
|
|
210
|
+
}
|
|
211
|
+
else if (char === '}') {
|
|
212
|
+
if (braceBalance > 0) {
|
|
213
|
+
braceBalance--;
|
|
214
|
+
}
|
|
215
|
+
// 如果 braceBalance 已经是 0,遇到了多余的 },这里选择忽略它(不处理),
|
|
216
|
+
// 因为删除可能会破坏用户意图,且通常 MathLive 能容忍多余的闭合
|
|
217
|
+
}
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
// 补全剩余的花括号
|
|
221
|
+
while (braceBalance > 0) {
|
|
222
|
+
current += '}';
|
|
223
|
+
braceBalance--;
|
|
224
|
+
}
|
|
225
|
+
return current;
|
|
226
|
+
};
|
|
227
|
+
if (text) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
let latexToInsert = text;
|
|
230
|
+
const hasChinese = /[\u4E00-\u9FA5]/.test(text);
|
|
231
|
+
// 检查是否包含LaTeX分隔符或命令(不仅仅是$...$或\(...\))
|
|
232
|
+
const hasDollar = text.includes('$');
|
|
233
|
+
const hasParen = text.includes('\\(') && text.includes('\\)');
|
|
234
|
+
const hasBracket = text.includes('\\[') && text.includes('\\]');
|
|
235
|
+
// 扩展 LaTeX 命令列表,包含更多常用数学符号和命令,以避免误判为普通文本
|
|
236
|
+
const latexCommandRegex = /\\(?:text|mathrm|mathbf|frac|sum|int|lim|alpha|beta|gamma|delta|theta|pi|phi|omega|Delta|Phi|Omega|hat|tilde|dot|ddot|vec|over|under|substack|left|right|begin|end|xlongequal|xrightarrow|xleftarrow|bar|sqrt|pm|times|div|cdot|leq|geq|neq|approx|infty|partial)\b/;
|
|
237
|
+
const hasLatexCommands = latexCommandRegex.test(text);
|
|
238
|
+
const hasDelimiters = hasDollar || hasParen || hasBracket || hasLatexCommands;
|
|
239
|
+
// 检查是否包含化学反应箭头命令,如果是,则整个文本应作为单一公式处理
|
|
240
|
+
const hasChemicalArrow = /\\x(?:longequal|xrightarrow|xleftarrow)/.test(text);
|
|
241
|
+
if (hasChemicalArrow) {
|
|
242
|
+
// 化学方程式,整个内容作为单一公式处理,不需要额外处理
|
|
243
|
+
latexToInsert = text;
|
|
244
|
+
}
|
|
245
|
+
else if (hasDelimiters) {
|
|
246
|
+
// 处理混合模式:将普通文本包裹在 \text{} 中,公式部分保留
|
|
247
|
+
// 首先使用辅助函数分割LaTeX命令和其他部分
|
|
248
|
+
const parts = splitWithLatexCommands(text);
|
|
249
|
+
latexToInsert = parts
|
|
250
|
+
.map((part) => {
|
|
251
|
+
// 检查是否是LaTeX命令(如\xlongequal{...})
|
|
252
|
+
if (/^\\x(?:longequal|xrightarrow|xleftarrow)\{(?:[^{}]|\{[^{}]*\})*\}$/.test(part)) {
|
|
253
|
+
// LaTeX命令部分,直接返回
|
|
254
|
+
return part;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// 非LaTeX命令部分,可能是普通文本或其他分隔符,继续处理
|
|
258
|
+
// 先分割其他类型的分隔符
|
|
259
|
+
const subParts = part.split(/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\(.+?\\\)|(\$[^$]+\$))/g);
|
|
260
|
+
return subParts
|
|
261
|
+
.map((subPart) => {
|
|
262
|
+
if (!subPart)
|
|
263
|
+
return '';
|
|
264
|
+
// 检查是否是 LaTeX 分隔符包围的内容(如 \[...\], \(...\), $$...$$, $...$)
|
|
265
|
+
const isDelimitedFormula = (subPart.startsWith('$$') && subPart.endsWith('$$'))
|
|
266
|
+
|| (subPart.startsWith('\\[') && subPart.endsWith('\\]'))
|
|
267
|
+
|| (subPart.startsWith('\\(') && subPart.endsWith('\\)'))
|
|
268
|
+
|| (subPart.startsWith('$') && subPart.endsWith('$'));
|
|
269
|
+
if (isDelimitedFormula) {
|
|
270
|
+
// 对于已分隔的公式,移除分隔符并递归清理内部的 LaTeX 分隔符
|
|
271
|
+
let content = subPart;
|
|
272
|
+
let clean = true;
|
|
273
|
+
while (clean) {
|
|
274
|
+
clean = false;
|
|
275
|
+
content = content.trim();
|
|
276
|
+
if (content.startsWith('$$') && content.endsWith('$$')) {
|
|
277
|
+
content = content.slice(2, -2);
|
|
278
|
+
clean = true;
|
|
279
|
+
}
|
|
280
|
+
else if (content.startsWith('\\[') && content.endsWith('\\]')) {
|
|
281
|
+
content = content.slice(2, -2);
|
|
282
|
+
clean = true;
|
|
283
|
+
}
|
|
284
|
+
else if (content.startsWith('\\(') && content.endsWith('\\)')) {
|
|
285
|
+
content = content.slice(2, -2);
|
|
286
|
+
clean = true;
|
|
287
|
+
}
|
|
288
|
+
else if (content.startsWith('$') && content.endsWith('$') && content.length > 2) {
|
|
289
|
+
content = content.slice(1, -1);
|
|
290
|
+
clean = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return content;
|
|
294
|
+
}
|
|
295
|
+
// 如果 subPart 包含明显的 LaTeX 命令,且没有外层分隔符,则直接视为公式返回,不包裹 \text{}
|
|
296
|
+
// 避免将类似 \bar{d}Q 这样的裸公式错误地转义为 \text{\bar{d}Q}
|
|
297
|
+
if (latexCommandRegex.test(subPart)) {
|
|
298
|
+
return subPart;
|
|
299
|
+
}
|
|
300
|
+
// 是非公式文本,处理换行并包裹 \text{}
|
|
301
|
+
// 如果文本全是空白字符,直接返回空格
|
|
302
|
+
if (!subPart.trim()) {
|
|
303
|
+
return '';
|
|
304
|
+
}
|
|
305
|
+
return subPart
|
|
306
|
+
.split(/\r?\n/g)
|
|
307
|
+
.map((line) => {
|
|
308
|
+
if (!line)
|
|
309
|
+
return '';
|
|
310
|
+
// 转义特殊字符,但排除已经转义的 LaTeX 命令
|
|
311
|
+
// 避免转义 LaTeX 命令中的反斜杠
|
|
312
|
+
const escaped = line.replace(/([^\\])([{}%#&])/g, '$1\\$2'); // 只转义未被转义的特殊字符
|
|
313
|
+
return line.trim() ? `\\text{${escaped}}` : '';
|
|
314
|
+
})
|
|
315
|
+
.join(' ');
|
|
316
|
+
})
|
|
317
|
+
.join('');
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
.join('');
|
|
321
|
+
}
|
|
322
|
+
else if (hasChinese) {
|
|
323
|
+
// 纯文本(含中文),整体包裹,并处理换行
|
|
324
|
+
latexToInsert = text
|
|
325
|
+
.split(/\r?\n/g)
|
|
326
|
+
.map((line) => {
|
|
327
|
+
const escaped = line.replace(/([\\{}])/g, '\\$1');
|
|
328
|
+
return `\\text{${escaped}}`;
|
|
329
|
+
})
|
|
330
|
+
.join(' ');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// 其他情况(纯公式或英文),仅处理换行
|
|
334
|
+
latexToInsert = text.replace(/\r?\n/g, ' ');
|
|
335
|
+
}
|
|
336
|
+
// 自动修复不平衡的大括号
|
|
337
|
+
latexToInsert = fixLatex(latexToInsert);
|
|
338
|
+
mf.executeCommand(['insert', latexToInsert]);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
mf.addEventListener('input', onInput);
|
|
342
|
+
mf.addEventListener('paste', onPaste);
|
|
343
|
+
cleanup = () => {
|
|
344
|
+
mf.removeEventListener('input', onInput);
|
|
345
|
+
mf.removeEventListener('paste', onPaste);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Retry if ref is not ready
|
|
350
|
+
setTimeout(initListeners, 100);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// Wait for custom element definition then init
|
|
354
|
+
if (customElements.get('math-field')) {
|
|
355
|
+
initListeners();
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
customElements.whenDefined('math-field').then(initListeners);
|
|
359
|
+
}
|
|
360
|
+
return () => {
|
|
361
|
+
isMounted = false;
|
|
362
|
+
if (cleanup)
|
|
363
|
+
cleanup();
|
|
364
|
+
};
|
|
365
|
+
}, []);
|
|
366
|
+
const onSelect = (latex) => {
|
|
367
|
+
if (editMode === 'mathlive') {
|
|
368
|
+
if (mfRef.current) {
|
|
369
|
+
mfRef.current.focus();
|
|
370
|
+
mfRef.current.executeCommand(['insert', latex]);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
setLatexVal(prev => prev + latex);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const onLatexChange = (e) => {
|
|
378
|
+
setLatexVal(e.target.value);
|
|
379
|
+
};
|
|
380
|
+
const renderPreview = (text) => {
|
|
381
|
+
if (!text)
|
|
382
|
+
return '';
|
|
383
|
+
// 混合解析正则
|
|
384
|
+
// 匹配 \[...\], $$...$$, \(...\), $...$
|
|
385
|
+
const regex = /(\\\[[\s\S]*?\\\])|(\$\$[\s\S]*?\$\$)|(\\\([\s\S]*?\\\))|(\$[^$]+\$)/g;
|
|
386
|
+
// 如果没有匹配到任何定界符
|
|
387
|
+
if (!text.match(regex)) {
|
|
388
|
+
// 尝试作为整体渲染(处理裸公式的情况)
|
|
389
|
+
const seemsLikeLatex = text.includes('\\') || text.includes('$');
|
|
390
|
+
if (seemsLikeLatex) {
|
|
391
|
+
try {
|
|
392
|
+
return katex_1.default.renderToString(text, { throwOnError: true, strict: false, trust: true });
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
// 如果整体渲染失败,可能只是包含 \ 的普通文本,或者是无法识别的公式
|
|
396
|
+
// 直接返回原文本(简单转义)
|
|
397
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// 纯文本
|
|
401
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
402
|
+
}
|
|
403
|
+
let lastIndex = 0;
|
|
404
|
+
let result = '';
|
|
405
|
+
let match = regex.exec(text);
|
|
406
|
+
while (match !== null) {
|
|
407
|
+
// 添加匹配之前的文本
|
|
408
|
+
const textPart = text.slice(lastIndex, match.index);
|
|
409
|
+
result += textPart.replace(/</g, '<').replace(/>/g, '>');
|
|
410
|
+
const fullMatch = match[0];
|
|
411
|
+
let formula = '';
|
|
412
|
+
let displayMode = false;
|
|
413
|
+
if (fullMatch.startsWith('\\[')) {
|
|
414
|
+
formula = fullMatch.slice(2, -2);
|
|
415
|
+
displayMode = true;
|
|
416
|
+
}
|
|
417
|
+
else if (fullMatch.startsWith('$$')) {
|
|
418
|
+
formula = fullMatch.slice(2, -2);
|
|
419
|
+
displayMode = true;
|
|
420
|
+
}
|
|
421
|
+
else if (fullMatch.startsWith('\\(')) {
|
|
422
|
+
formula = fullMatch.slice(2, -2);
|
|
423
|
+
displayMode = false;
|
|
424
|
+
}
|
|
425
|
+
else if (fullMatch.startsWith('$')) {
|
|
426
|
+
formula = fullMatch.slice(1, -1);
|
|
427
|
+
displayMode = false;
|
|
428
|
+
}
|
|
429
|
+
// 循环清理可能存在的嵌套定界符 (例如 $$ \(...\) $$)
|
|
430
|
+
let clean = true;
|
|
431
|
+
while (clean) {
|
|
432
|
+
clean = false;
|
|
433
|
+
formula = formula.trim();
|
|
434
|
+
if (formula.startsWith('\\(') && formula.endsWith('\\)')) {
|
|
435
|
+
formula = formula.slice(2, -2);
|
|
436
|
+
clean = true;
|
|
437
|
+
}
|
|
438
|
+
else if (formula.startsWith('\\[') && formula.endsWith('\\]')) {
|
|
439
|
+
formula = formula.slice(2, -2);
|
|
440
|
+
clean = true;
|
|
441
|
+
}
|
|
442
|
+
else if (formula.startsWith('$$') && formula.endsWith('$$')) {
|
|
443
|
+
formula = formula.slice(2, -2);
|
|
444
|
+
clean = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
result += katex_1.default.renderToString(formula, {
|
|
449
|
+
throwOnError: false,
|
|
450
|
+
displayMode,
|
|
451
|
+
strict: false,
|
|
452
|
+
trust: true,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
result += `<span style="color: red;">${fullMatch}</span>`;
|
|
457
|
+
}
|
|
458
|
+
lastIndex = regex.lastIndex;
|
|
459
|
+
match = regex.exec(text);
|
|
460
|
+
}
|
|
461
|
+
// 添加剩余的文本
|
|
462
|
+
result += text.slice(lastIndex).replace(/</g, '<').replace(/>/g, '>');
|
|
463
|
+
return result;
|
|
464
|
+
};
|
|
465
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "MathEditor", style: style, children: [(0, jsx_runtime_1.jsxs)("div", { className: "mathBody", children: [(0, jsx_runtime_1.jsx)("div", { className: "modeSegmented", children: (0, jsx_runtime_1.jsx)(antd_1.Segmented, { value: editMode, onChange: val => handleModeChange(val.toString()), options: [
|
|
466
|
+
{ label: 'MathType 编辑', value: 'mathlive' },
|
|
467
|
+
{ label: 'LaTex 编辑', value: 'latex' },
|
|
468
|
+
] }) }), (0, jsx_runtime_1.jsx)("div", { className: "editContent", children: editMode === 'mathlive'
|
|
469
|
+
? ((0, jsx_runtime_1.jsxs)("div", { className: "editKatax", children: [(0, jsx_runtime_1.jsx)("p", { className: "title", children: "\u516C\u5F0F\u7F16\u8F91\u533A\u57DF\uFF08\u652F\u6301\u76F4\u63A5\u7C98\u8D34\u516C\u5F0F\uFF09\uFF1A" }), (0, jsx_runtime_1.jsx)("math-field", { ref: mfRef, className: "mathField" })] }))
|
|
470
|
+
: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "editKatax", children: [(0, jsx_runtime_1.jsx)("p", { className: "title", children: "latex\u7F16\u8F91\u533A\u57DF\uFF1A\u6682\u4E0D\u652F\u6301\u4E2D\u6587\u5B57\u7B26\uFF0C\u8BF7\u4E0D\u8981\u8F93\u5165\u4E2D\u6587\u5B57\u7B26" }), (0, jsx_runtime_1.jsx)(antd_1.Input.TextArea, { value: latexVal, onChange: onLatexChange, style: { height: '75px' } })] }), (0, jsx_runtime_1.jsxs)("div", { className: "previewKatax", children: [(0, jsx_runtime_1.jsx)("p", { className: "previewTitle", children: "\u516C\u5F0F\u9884\u89C8\uFF1A" }), (0, jsx_runtime_1.jsx)("div", { dangerouslySetInnerHTML: {
|
|
471
|
+
__html: renderPreview(latexVal),
|
|
472
|
+
} })] })] })) })] }), (0, jsx_runtime_1.jsx)(antd_1.Tabs, { activeKey: activeKey, onChange: setActiveKey, className: "mathTabs", popupClassName: "MathEditorMorePopup", items: const_1.KATEXLIST.map(i => ({
|
|
473
|
+
label: i.name,
|
|
474
|
+
key: i.name,
|
|
475
|
+
children: ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: i.children.map((c, idx) => ((0, jsx_runtime_1.jsx)("div", { className: "kataxBody", onClick: () => onSelect(c), dangerouslySetInnerHTML: {
|
|
476
|
+
__html: katex_1.default.renderToString(c),
|
|
477
|
+
} }, `${i.name}-${idx}`))) })),
|
|
478
|
+
})) })] }));
|
|
479
|
+
};
|
|
480
|
+
exports.default = MathEditor;
|