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