Skip to content

Commit 0a07ee8

Browse files
Sanitize LaTeX before rendering; fall back to source text on parse error
- Some informal descriptions contain \mathbb{X} inside \text{...}, which KaTeX (correctly) refuses to parse. Rewrite those cases to the Unicode equivalent (ℝ, ℂ, ℤ, ℕ, ℚ, etc.) before rendering so the text renders instead of showing as a red error. - Flip throwOnError to true and fall back to escaped plain source on failure, so truly unparseable LaTeX degrades to readable text rather than KaTeX's red inline error. - Set strict: "ignore" so minor parser warnings don't trip rendering.
1 parent c48504d commit 0a07ee8

1 file changed

Lines changed: 40 additions & 5 deletions

File tree

frontend/src/components/LatexText.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type Part =
1414

1515
function parseParts(text: string): Part[] {
1616
const parts: Part[] = [];
17-
// Match $$...$$ first (display), then $...$ (inline)
18-
const re = /(\$\$[\s\S]*?\$\$|\$[^$\n]+?\$)/g;
17+
// Match $$...$$, \[...\] (display), then \(...\), $...$ (inline)
18+
const re = /(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^$\n]+?\$)/g;
1919
let last = 0;
2020
let match: RegExpExecArray | null;
2121

@@ -26,6 +26,10 @@ function parseParts(text: string): Part[] {
2626
const raw = match[0];
2727
if (raw.startsWith("$$")) {
2828
parts.push({ type: "block", latex: raw.slice(2, -2) });
29+
} else if (raw.startsWith("\\[")) {
30+
parts.push({ type: "block", latex: raw.slice(2, -2) });
31+
} else if (raw.startsWith("\\(")) {
32+
parts.push({ type: "inline", latex: raw.slice(2, -2) });
2933
} else {
3034
parts.push({ type: "inline", latex: raw.slice(1, -1) });
3135
}
@@ -38,15 +42,46 @@ function parseParts(text: string): Part[] {
3842
return parts;
3943
}
4044

45+
function escapeHtml(s: string): string {
46+
return s
47+
.replace(/&/g, "&")
48+
.replace(/</g, "&lt;")
49+
.replace(/>/g, "&gt;")
50+
.replace(/"/g, "&quot;")
51+
.replace(/'/g, "&#39;");
52+
}
53+
54+
const MATHBB_UNICODE: Record<string, string> = {
55+
R: "ℝ", C: "ℂ", Z: "ℤ", N: "ℕ", Q: "ℚ",
56+
P: "ℙ", H: "ℍ", F: "𝔽", K: "𝕂",
57+
};
58+
59+
// The informal-description generator sometimes emits math-mode commands like
60+
// `\mathbb{R}` inside `\text{...}`, which KaTeX (correctly) refuses to render.
61+
// Replace those with their Unicode equivalents so the text renders cleanly.
62+
function sanitizeLatex(latex: string): string {
63+
const textGroup = /\\text\{((?:[^{}]|\{[^{}]*\})*)\}/g;
64+
return latex.replace(textGroup, (_match, inner: string) => {
65+
const fixed = inner.replace(
66+
/\\mathbb\{([A-Z])\}/g,
67+
(m, letter: string) => MATHBB_UNICODE[letter] ?? m
68+
);
69+
return `\\text{${fixed}}`;
70+
});
71+
}
72+
4173
function renderLatex(latex: string, displayMode: boolean): string {
74+
const source = sanitizeLatex(latex);
4275
try {
43-
return katex.renderToString(latex, {
76+
return katex.renderToString(source, {
4477
displayMode,
45-
throwOnError: false,
78+
throwOnError: true,
79+
strict: "ignore",
4680
trust: false,
4781
});
4882
} catch {
49-
return latex;
83+
const [open, close] = displayMode ? ["\\[", "\\]"] : ["$", "$"];
84+
return escapeHtml(`${open}${latex}${close}`);
5085
}
5186
}
5287

0 commit comments

Comments
 (0)