tahamajs/IE / Projects /front /src /components /article /MathRenderer.tsx
tahamajs's picture
download
raw
3.6 kB
// 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 "&amp;";
if (m === "<") return "&lt;";
if (m === ">") return "&gt;";
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.