114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
||
|
||
declare global {
|
||
interface Window {
|
||
MathJax?: {
|
||
startup?: { promise?: Promise<void> };
|
||
typesetPromise?: (elements?: Element[]) => Promise<void>;
|
||
[key: string]: any;
|
||
};
|
||
}
|
||
}
|
||
|
||
interface MediaFile {
|
||
id: number;
|
||
fileName: string;
|
||
mediaUrl: string;
|
||
}
|
||
|
||
interface LaTextContainerProps {
|
||
html: string;
|
||
latex: string;
|
||
mediaFiles?: MediaFile[];
|
||
}
|
||
|
||
let mathJaxPromise: Promise<void> | null = null;
|
||
|
||
const loadMathJax = () => {
|
||
if (mathJaxPromise) return mathJaxPromise;
|
||
|
||
mathJaxPromise = new Promise<void>((resolve, reject) => {
|
||
if (window.MathJax?.typesetPromise) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
(window as any).MathJax = {
|
||
tex: {
|
||
inlineMath: [["$$$", "$$$"]],
|
||
displayMath: [["$$$$$$", "$$$$$$"]],
|
||
processEscapes: true,
|
||
},
|
||
options: {
|
||
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
||
},
|
||
startup: { typeset: false },
|
||
};
|
||
|
||
const script = document.createElement("script");
|
||
script.id = "mathjax-script";
|
||
script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";
|
||
script.async = true;
|
||
|
||
script.onload = () => {
|
||
window.MathJax?.startup?.promise?.then(resolve).catch(reject);
|
||
};
|
||
|
||
script.onerror = reject;
|
||
document.head.appendChild(script);
|
||
});
|
||
|
||
return mathJaxPromise;
|
||
};
|
||
|
||
const replaceImages = (html: string, latex: string, mediaFiles?: MediaFile[]) => {
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, "text/html");
|
||
|
||
const latexImageNames = Array.from(latex.matchAll(/\\includegraphics\{(.+?)\}/g)).map(
|
||
(match) => match[1]
|
||
);
|
||
|
||
const imgs = doc.querySelectorAll<HTMLImageElement>("img.tex-graphics");
|
||
|
||
imgs.forEach((img, idx) => {
|
||
const imageName = latexImageNames[idx];
|
||
if (!imageName || !mediaFiles) return;
|
||
const mediaFile = mediaFiles.find((f) => f.fileName === imageName);
|
||
if (mediaFile) img.src = mediaFile.mediaUrl;
|
||
});
|
||
|
||
return doc.body.innerHTML;
|
||
};
|
||
|
||
const LaTextContainer: React.FC<LaTextContainerProps> = ({ html, latex, mediaFiles }) => {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [processedHtml, setProcessedHtml] = useState<string>(html);
|
||
|
||
// 1️⃣ Обновляем HTML при изменении входных данных
|
||
useEffect(() => {
|
||
setProcessedHtml(replaceImages(html, latex, mediaFiles));
|
||
}, [html, latex, mediaFiles]);
|
||
|
||
// 2️⃣ После рендера обновленного HTML применяем MathJax
|
||
useEffect(() => {
|
||
const renderMath = () => {
|
||
if (containerRef.current && window.MathJax?.typesetPromise) {
|
||
window.MathJax.typesetPromise([containerRef.current]).catch(console.error);
|
||
}
|
||
};
|
||
|
||
loadMathJax().then(renderMath).catch(console.error);
|
||
}, [processedHtml]); // 👈 ключевой момент — триггерим именно по processedHtml
|
||
|
||
return (
|
||
<div
|
||
className="latex-container"
|
||
ref={containerRef}
|
||
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
||
/>
|
||
);
|
||
};
|
||
|
||
export default LaTextContainer;
|