@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.
@@ -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
- let currentInline = initialData?.inline !== false;
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