import { Node as PMNode } from 'prosemirror-model'; import { NodeView } from 'prosemirror-view'; /** Turns a LaTeX string into HTML (KaTeX/MathJax/MathML). Host-provided. */ export type MathRenderer = (latex: string, display: 'inline' | 'block') => string; /** * Lazy-loaded KaTeX module (loaded once, only when the first formula renders, * so editors without math pay nothing). Resolves to the katex default export. */ let katexPromise: Promise<{ renderToString(tex: string, opts?: Record): string }> | null = null; function loadKatex() { if (!katexPromise) { katexPromise = import('katex').then(m => m.default); } return katexPromise; } /** * Inline atom rendering a math formula. Precedence: * 1. A host-supplied `mathRenderer` (full control — e.g. KaTeX HTML, MathJax). * 2. The built-in default: KaTeX compiled to **MathML**, which browsers render * natively with no stylesheet or web fonts (works cleanly inside the * editor's shadow root). * 3. If KaTeX can't load / parse, the raw LaTeX stays visible. */ export class MathView implements NodeView { dom: HTMLSpanElement; private renderer?: MathRenderer; /** Bumps on every render so a slow async KaTeX result for a stale node is dropped. */ private token = 0; constructor(node: PMNode, renderer?: MathRenderer) { this.renderer = renderer; this.dom = document.createElement('span'); this.dom.contentEditable = 'false'; this.render(node); } private render(node: PMNode): void { const latex = (node.attrs.latex as string) ?? ''; const display = (node.attrs.display as 'inline' | 'block') ?? 'inline'; this.dom.className = display === 'block' ? 'nile-wysiwyg__math nile-wysiwyg__math--block' : 'nile-wysiwyg__math'; this.dom.setAttribute('data-math', latex); this.dom.setAttribute('data-math-display', display); this.dom.title = latex; const token = ++this.token; // 1. Host renderer wins. if (this.renderer) { try { // Host renderer output is trusted (same trust level as uploadHandler). this.dom.innerHTML = this.renderer(latex, display); return; } catch { // Fall through to the built-in renderer. } } // 2. Built-in KaTeX → MathML. Show raw LaTeX until the (lazy) engine loads. this.dom.textContent = latex; loadKatex() .then(katex => { if (token !== this.token) return; // node changed while loading try { this.dom.innerHTML = katex.renderToString(latex, { output: 'mathml', displayMode: display === 'block', throwOnError: false, }); } catch { this.dom.textContent = latex; } }) .catch(() => { /* KaTeX unavailable — keep the raw LaTeX already shown. */ }); } update(node: PMNode): boolean { if (node.type.name !== 'math') return false; this.render(node); return true; } /** Atom node: ProseMirror owns selection; never read our subtree back. */ ignoreMutation(): boolean { return true; } }