);
}
const ALIGN_MAP = {
left: 'flex-start',
center: 'center',
right: 'flex-end',
} as const;
function KatexContent({
html,
width,
height,
align = 'center',
}: {
html: string;
width: number;
height: number;
align?: 'left' | 'center' | 'right';
}) {
const innerRef = useRef(null);
const [scale, setScale] = useState(1);
// Measure the formula's natural size and shrink-to-fit its box. Extracted so
// it can run both synchronously on mount (useLayoutEffect) and again once the
// KaTeX fonts finish loading (useEffect below).
//
// KaTeX lays out large delimiters (\left\{, \begin{cases}) using the metrics of
// its `KaTeX_Size1`–`Size4` faces. Those woff2 files load asynchronously, so a
// first measure on a cold export runs against the fallback font — the brace is
// sized wrong and the piecewise body (`cases`) desynchronizes from it (the bug
// reported as "大括号后面的分段函数与大括号错位"). Re-measuring after
// `document.fonts.ready` snaps the scale back to the real glyph metrics before
// the off-screen snapshot captures the frame.
const measure = useCallback(() => {
if (!innerRef.current) return;
const naturalW = innerRef.current.scrollWidth;
const naturalH = innerRef.current.scrollHeight;
if (naturalW > 0 && naturalH > 0) {
// Cap at 1: only ever shrink the formula to fit its box, never enlarge.
// A short formula sitting in a large frame (e.g. slide 29 的右侧推理框)
// would otherwise get scaled up to fill the box and render huge.
setScale(Math.min(width / naturalW, height / naturalH, 1));
}
}, [width, height]);
useLayoutEffect(() => {
measure();
}, [measure, html]);
useEffect(() => {
const fonts = typeof document !== 'undefined' ? document.fonts : undefined;
if (!fonts) return;
let cancelled = false;
// Re-measure once the font set settles (initial + any subsequent face that
// finishes decoding), so the fit-scale reflects the real KaTeX_Size metrics.
fonts.ready.then(() => {
if (!cancelled) measure();
});
const onLoadingDone = () => measure();
fonts.addEventListener?.('loadingdone', onLoadingDone);
return () => {
cancelled = true;
fonts.removeEventListener?.('loadingdone', onLoadingDone);
};
}, [measure, html]);
const justify = ALIGN_MAP[align];
const origin =
align === 'left' ? 'left center' : align === 'right' ? 'right center' : 'center center';
return (