| // src/components/article/MathRenderer.tsx | |
| "use client"; | |
| import katex from "katex"; | |
| import DOMPurify from "dompurify"; | |
| import { useMemo } from "react"; | |
| interface MathRendererProps { | |
| content: string; | |
| trustHtml?: boolean; // if true, allow raw HTML (sanitised) | |
| } | |
| // Simple cache for rendered formulas | |
| const cache = new Map<string, string>(); | |
| function renderFormula(latex: string, displayMode: boolean): string { | |
| const key = `${displayMode ? "d" : "i"}:${latex}`; | |
| if (cache.has(key)) return cache.get(key)!; | |
| try { | |
| const html = katex.renderToString(latex, { | |
| throwOnError: false, | |
| displayMode, | |
| output: "html", | |
| strict: false, | |
| }); | |
| cache.set(key, html); | |
| return html; | |
| } catch (error) { | |
| console.error("KaTeX error:", error); | |
| const errorMsg = `[LaTeX Error: ${latex.substring(0, 50)}]`; | |
| cache.set(key, errorMsg); | |
| return errorMsg; | |
| } | |
| } | |
| // Split string by regex while preserving delimiters | |
| function splitByDelimiters(text: string, delimiters: { open: string; close: string; display: boolean }[]) { | |
| const parts: { type: "text" | "math"; content: string; display: boolean }[] = []; | |
| let lastIndex = 0; | |
| const combinedRegex = new RegExp( | |
| delimiters.map(d => `${escapeRegex(d.open)}(.*?)${escapeRegex(d.close)}`).join("|"), | |
| "gs" | |
| ); | |
| let match; | |
| while ((match = combinedRegex.exec(text)) !== null) { | |
| const matchStart = match.index; | |
| const matchEnd = matchStart + match[0].length; | |
| // preceding text | |
| if (matchStart > lastIndex) { | |
| parts.push({ type: "text", content: text.slice(lastIndex, matchStart), display: false }); | |
| } | |
| // determine which delimiter matched | |
| let idx = 1; | |
| for (let i = 0; i < delimiters.length; i++) { | |
| if (match[idx]) { | |
| parts.push({ | |
| type: "math", | |
| content: match[idx].trim(), | |
| display: delimiters[i].display, | |
| }); | |
| break; | |
| } | |
| idx += 2; | |
| } | |
| lastIndex = matchEnd; | |
| } | |
| // remaining text | |
| if (lastIndex < text.length) { | |
| parts.push({ type: "text", content: text.slice(lastIndex), display: false }); | |
| } | |
| return parts; | |
| } | |
| function escapeRegex(str: string) { | |
| return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| } | |
| export function MathRenderer({ content, trustHtml = false }: MathRendererProps) { | |
| const processed = useMemo(() => { | |
| if (!content) return null; | |
| const delimiters = [ | |
| { open: "$$", close: "$$", display: true }, | |
| { open: "\\[", close: "\\]", display: true }, | |
| { open: "$", close: "$", display: false }, | |
| { open: "\\(", close: "\\)", display: false }, | |
| ]; | |
| const parts = splitByDelimiters(content, delimiters); | |
| return parts.map((part, idx) => { | |
| if (part.type === "math") { | |
| const html = renderFormula(part.content, part.display); | |
| return <span key={idx} dangerouslySetInnerHTML={{ __html: html }} />; | |
| } else { | |
| // Plain text – optionally sanitise if trustHtml is false | |
| let safeHtml = part.content; | |
| if (!trustHtml) { | |
| // Escape basic HTML to prevent XSS | |
| safeHtml = part.content.replace(/[&<>]/g, (m) => { | |
| if (m === "&") return "&"; | |
| if (m === "<") return "<"; | |
| if (m === ">") return ">"; | |
| return m; | |
| }); | |
| } else if (trustHtml) { | |
| // Sanitise with DOMPurify (only if we allow HTML) | |
| safeHtml = DOMPurify.sanitize(part.content); | |
| } | |
| return <span key={idx} dangerouslySetInnerHTML={{ __html: safeHtml }} />; | |
| } | |
| }); | |
| }, [content, trustHtml]); | |
| return <>{processed}</>; | |
| } |
Xet Storage Details
- Size:
- 3.6 kB
- Xet hash:
- b9ec5f7970d48c9a00bbc6ea0654f2aa2dd31a02f0a946e42076b7b20c13a296
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.