@editora/math 1.0.1 → 1.0.3
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/dist/index.cjs.js +62 -0
- package/dist/index.esm.js +492 -0
- package/package.json +3 -3
- package/src/MathPlugin.native.ts +58 -6
- package/src/MathPlugin.ts +0 -1099
- package/src/libs/mathml-converter.ts +0 -433
package/src/MathPlugin.native.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from '../../shared/escapeHtml';
|
|
2
|
+
import { initDialogOverlay } from '../../shared/dialogHelpers';
|
|
1
3
|
import type { Plugin } from '@editora/core';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -40,6 +42,53 @@ let selectedMathElement: HTMLElement | null = null;
|
|
|
40
42
|
const DARK_THEME_SELECTOR = '[data-theme="dark"], .dark, .editora-theme-dark';
|
|
41
43
|
|
|
42
44
|
// Global flag to ensure listener is added only once across all instances
|
|
45
|
+
const MATHML_ELEMENTS = new Set([
|
|
46
|
+
'math', 'mrow', 'mi', 'mn', 'mo', 'ms', 'mtext', 'mspace', 'msup', 'msub', 'msubsup', 'mfrac', 'msqrt',
|
|
47
|
+
'mroot', 'mfenced', 'mstyle', 'mpadded', 'mphantom', 'menclose', 'mtable', 'mtr', 'mtd', 'mlabeledtr',
|
|
48
|
+
'munder', 'mover', 'munderover', 'mmultiscripts', 'mprescripts', 'none', 'semantics', 'annotation',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const MATHML_ATTRIBUTES = new Set([
|
|
52
|
+
'display', 'displaystyle', 'mathvariant', 'mathsize', 'mathcolor', 'mathbackground', 'scriptlevel',
|
|
53
|
+
'fence', 'separator', 'stretchy', 'symmetric', 'lspace', 'rspace', 'largeop', 'movablelimits',
|
|
54
|
+
'accent', 'accentunder', 'open', 'close', 'separators', 'linethickness', 'columnalign', 'rowalign',
|
|
55
|
+
'columnspacing', 'rowspacing', 'columnlines', 'rowlines', 'frame', 'notation', 'width', 'height',
|
|
56
|
+
'depth', 'voffset', 'minsize', 'maxsize', 'form', 'encoding', 'xmlns', 'dir', 'class', 'id',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reduce user-entered or document-stored MathML to a whitelist of MathML elements and
|
|
61
|
+
* attributes before it is assigned to innerHTML. The formula in the dialog's preview (and in
|
|
62
|
+
* Edit mode) can come from the document, and a bare `<img onerror>` or `<script>` inside
|
|
63
|
+
* "MathML" would otherwise run. Parsing happens in an inert DOMParser document so nothing
|
|
64
|
+
* executes while it is being inspected.
|
|
65
|
+
*/
|
|
66
|
+
const sanitizeMathMl = (markup: string): string => {
|
|
67
|
+
const doc = new DOMParser().parseFromString(`<body>${markup}</body>`, 'text/html');
|
|
68
|
+
|
|
69
|
+
const clean = (node: Node): void => {
|
|
70
|
+
Array.from(node.childNodes).forEach((child) => {
|
|
71
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
72
|
+
const element = child as Element;
|
|
73
|
+
if (!MATHML_ELEMENTS.has(element.localName.toLowerCase())) {
|
|
74
|
+
// Drop the element entirely, including anything nested inside it.
|
|
75
|
+
element.remove();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
79
|
+
if (!MATHML_ATTRIBUTES.has(attr.name.toLowerCase())) element.removeAttribute(attr.name);
|
|
80
|
+
});
|
|
81
|
+
clean(element);
|
|
82
|
+
} else if (child.nodeType !== Node.TEXT_NODE) {
|
|
83
|
+
child.parentNode?.removeChild(child);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
clean(doc.body);
|
|
88
|
+
|
|
89
|
+
return doc.body.innerHTML;
|
|
90
|
+
};
|
|
91
|
+
|
|
43
92
|
declare global {
|
|
44
93
|
interface Window {
|
|
45
94
|
__mathPluginDoubleClickInitialized?: boolean;
|
|
@@ -269,6 +318,8 @@ const showMathDialog = async (
|
|
|
269
318
|
initialData?: { formula: string; format: 'latex' | 'mathml'; inline: boolean },
|
|
270
319
|
preferredEditorContent?: HTMLElement | null,
|
|
271
320
|
) => {
|
|
321
|
+
document.querySelectorAll('.math-dialog-overlay').forEach((el) => el.remove());
|
|
322
|
+
|
|
272
323
|
const editorContent =
|
|
273
324
|
preferredEditorContent ||
|
|
274
325
|
(editingMathElement?.closest('.rte-content, .editora-content') as HTMLElement | null) ||
|
|
@@ -333,6 +384,7 @@ const showMathDialog = async (
|
|
|
333
384
|
};
|
|
334
385
|
|
|
335
386
|
const overlay = document.createElement('div');
|
|
387
|
+
initDialogOverlay(overlay);
|
|
336
388
|
overlay.className = 'math-dialog-overlay';
|
|
337
389
|
overlay.style.cssText = `position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: ${palette.overlay}; display: flex; align-items: center; justify-content: center; z-index: 99999;`;
|
|
338
390
|
|
|
@@ -341,7 +393,7 @@ const showMathDialog = async (
|
|
|
341
393
|
|
|
342
394
|
let currentFormat: 'latex' | 'mathml' = initialData?.format || 'latex';
|
|
343
395
|
let currentFormula = initialData?.formula || '';
|
|
344
|
-
|
|
396
|
+
const currentInline = initialData?.inline !== false;
|
|
345
397
|
let previewRaf: number | null = null;
|
|
346
398
|
let lastPreviewSignature = '';
|
|
347
399
|
|
|
@@ -371,7 +423,7 @@ const showMathDialog = async (
|
|
|
371
423
|
|
|
372
424
|
<div style="margin-bottom: 20px;">
|
|
373
425
|
<label style="display: block; font-weight: 600; margin-bottom: 8px; font-size: 14px; color: ${palette.text};">Formula:</label>
|
|
374
|
-
<textarea id="formula-input" rows="4" style="width: 100%; min-height: 112px; padding: 10px 12px; border: 1px solid ${palette.fieldBorder}; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.45; background: ${palette.fieldBg}; color: ${palette.text}; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; resize: vertical;">${currentFormula}</textarea>
|
|
426
|
+
<textarea id="formula-input" rows="4" style="width: 100%; min-height: 112px; padding: 10px 12px; border: 1px solid ${palette.fieldBorder}; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.45; background: ${palette.fieldBg}; color: ${palette.text}; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; resize: vertical;">${escapeHtml(currentFormula)}</textarea>
|
|
375
427
|
</div>
|
|
376
428
|
|
|
377
429
|
<div style="margin-bottom: 20px;">
|
|
@@ -438,10 +490,10 @@ const showMathDialog = async (
|
|
|
438
490
|
} else {
|
|
439
491
|
// For MathML, check if it starts with <math>, if not, wrap it
|
|
440
492
|
if (formula.trim().startsWith('<math')) {
|
|
441
|
-
previewArea.innerHTML = formula;
|
|
493
|
+
previewArea.innerHTML = sanitizeMathMl(formula);
|
|
442
494
|
} else {
|
|
443
495
|
// Wrap in <math> tag and render
|
|
444
|
-
previewArea.innerHTML = `<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline">${formula}</math
|
|
496
|
+
previewArea.innerHTML = sanitizeMathMl(`<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline">${formula}</math>`);
|
|
445
497
|
}
|
|
446
498
|
}
|
|
447
499
|
} catch {
|
|
@@ -550,11 +602,11 @@ const showMathDialog = async (
|
|
|
550
602
|
} else {
|
|
551
603
|
// For MathML, wrap in <math> tag if not already wrapped
|
|
552
604
|
if (formula.trim().startsWith('<math')) {
|
|
553
|
-
mathEl.innerHTML = formula;
|
|
605
|
+
mathEl.innerHTML = sanitizeMathMl(formula);
|
|
554
606
|
} else {
|
|
555
607
|
// Create proper MathML with namespace
|
|
556
608
|
const mathContent = `<math xmlns="http://www.w3.org/1998/Math/MathML" display="${mathData.inline ? 'inline' : 'block'}">${formula}</math>`;
|
|
557
|
-
mathEl.innerHTML = mathContent;
|
|
609
|
+
mathEl.innerHTML = sanitizeMathMl(mathContent);
|
|
558
610
|
}
|
|
559
611
|
}
|
|
560
612
|
|