|
1 | 1 | import type { MathData, MathInlineData } from "@wdprlib/ast"; |
2 | 2 | import type { RenderContext } from "../context"; |
3 | 3 | import { escapeAttr, escapeHtml } from "../escape"; |
| 4 | +import temml from "temml"; |
| 5 | + |
| 6 | +/** |
| 7 | + * Check if LaTeX needs to be wrapped in aligned environment. |
| 8 | + * Wikidot-style math blocks with & alignment markers need this. |
| 9 | + */ |
| 10 | +function needsAlignedWrapper(latex: string): boolean { |
| 11 | + // Already has an environment |
| 12 | + if (/\\begin\s*\{/.test(latex)) { |
| 13 | + return false; |
| 14 | + } |
| 15 | + // Has alignment marker (&) but not escaped (\&) - needs aligned environment |
| 16 | + // Remove escaped ampersands first, then check for unescaped ones |
| 17 | + const withoutEscaped = latex.replace(/\\&/g, ""); |
| 18 | + return withoutEscaped.includes("&"); |
| 19 | +} |
| 20 | + |
| 21 | +/** |
| 22 | + * Render LaTeX to MathML using temml. |
| 23 | + * Returns empty string on error. |
| 24 | + */ |
| 25 | +function renderLatexToMathML(latex: string, displayMode: boolean): string { |
| 26 | + try { |
| 27 | + // Wrap in aligned environment if needed for Wikidot-style alignment |
| 28 | + let processedLatex = latex; |
| 29 | + if (displayMode && needsAlignedWrapper(latex)) { |
| 30 | + processedLatex = `\\begin{aligned}\n${latex}\n\\end{aligned}`; |
| 31 | + } |
| 32 | + return temml.renderToString(processedLatex, { |
| 33 | + displayMode, |
| 34 | + throwOnError: false, |
| 35 | + annotate: false, |
| 36 | + }); |
| 37 | + } catch { |
| 38 | + return ""; |
| 39 | + } |
| 40 | +} |
4 | 41 |
|
5 | 42 | /** Render a block math element */ |
6 | 43 | export function renderMath(ctx: RenderContext, data: MathData): void { |
7 | 44 | const index = ctx.nextEquationIndex() + 1; |
| 45 | + const latex = data["latex-source"]; |
| 46 | + const mathml = renderLatexToMathML(latex, true); |
| 47 | + |
| 48 | + const id = data.name ? `equation-${data.name}` : `equation-${index}`; |
| 49 | + const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : ""; |
| 50 | + |
| 51 | + ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`); |
8 | 52 |
|
| 53 | + // Equation number (only for named equations) |
9 | 54 | if (data.name) { |
10 | 55 | ctx.push(`<span class="equation-number">(${index})</span>`); |
11 | 56 | } |
12 | 57 |
|
13 | | - const id = data.name ? `equation-${data.name}` : `equation-${index}`; |
14 | | - ctx.push(`<div class="math-equation" id="${escapeAttr(id)}">`); |
15 | | - ctx.push(escapeHtml(data["latex-source"])); |
| 58 | + // Hidden LaTeX source (for polyfill) |
| 59 | + ctx.push(`<code class="math-source" hidden aria-hidden="true">`); |
| 60 | + ctx.push(escapeHtml(latex)); |
| 61 | + ctx.push(`</code>`); |
| 62 | + |
| 63 | + // MathML output |
| 64 | + ctx.push(`<span class="math-render">`); |
| 65 | + if (mathml) { |
| 66 | + ctx.push(mathml); |
| 67 | + } else { |
| 68 | + // Fallback: display error |
| 69 | + ctx.push(`<span class="math-error">`); |
| 70 | + ctx.push(escapeHtml(latex)); |
| 71 | + ctx.push(`</span>`); |
| 72 | + } |
| 73 | + ctx.push(`</span>`); |
| 74 | + |
16 | 75 | ctx.push("</div>"); |
17 | 76 | } |
18 | 77 |
|
19 | 78 | /** Render an inline math element */ |
20 | 79 | export function renderMathInline(ctx: RenderContext, data: MathInlineData): void { |
21 | | - ctx.push(`<span class="math-inline">$`); |
22 | | - ctx.push(escapeHtml(data["latex-source"])); |
23 | | - ctx.push("$</span>"); |
| 80 | + const latex = data["latex-source"]; |
| 81 | + const mathml = renderLatexToMathML(latex, false); |
| 82 | + |
| 83 | + ctx.push(`<span class="math-inline">`); |
| 84 | + |
| 85 | + // Hidden LaTeX source (for polyfill) |
| 86 | + ctx.push(`<code class="math-source" hidden aria-hidden="true">`); |
| 87 | + ctx.push(escapeHtml(latex)); |
| 88 | + ctx.push(`</code>`); |
| 89 | + |
| 90 | + // MathML output |
| 91 | + ctx.push(`<span class="math-render">`); |
| 92 | + if (mathml) { |
| 93 | + ctx.push(mathml); |
| 94 | + } else { |
| 95 | + // Fallback: display with $ delimiters |
| 96 | + ctx.push(`<span class="math-error">$`); |
| 97 | + ctx.push(escapeHtml(latex)); |
| 98 | + ctx.push(`$</span>`); |
| 99 | + } |
| 100 | + ctx.push(`</span>`); |
| 101 | + |
| 102 | + ctx.push("</span>"); |
24 | 103 | } |
25 | 104 |
|
26 | 105 | /** Render an equation reference (link to named equation) */ |
27 | 106 | export function renderEquationRef(ctx: RenderContext, name: string): void { |
28 | | - // Create a link to the named equation |
29 | | - // The equation index is not available at render time without tracking, |
30 | | - // so we generate a link that can be resolved client-side or with post-processing |
31 | 107 | const id = `equation-${name}`; |
32 | | - ctx.push(`<a class="equation-ref" href="#${escapeAttr(id)}">`); |
| 108 | + ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`); |
| 109 | + ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`); |
33 | 110 | ctx.push(escapeHtml(name)); |
34 | | - ctx.push("</a>"); |
| 111 | + ctx.push(`</a>`); |
| 112 | + ctx.push(`<span class="eref-tooltip" aria-hidden="true"></span>`); |
| 113 | + ctx.push("</span>"); |
35 | 114 | } |
0 commit comments