Skip to content

Commit 8f12dc3

Browse files
committed
fix latext char display issue
1 parent 09f76a2 commit 8f12dc3

2 files changed

Lines changed: 150 additions & 38 deletions

File tree

public/excalidraw/test-2.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ tags: [excalidraw]
1010
# Excalidraw Data
1111

1212
## Text Elements
13+
中文 ^NEDv73Or
14+
15+
0123abc ^1TJ2uVPo
16+
1317
## Embedded Files
1418
db13bc49deb3ea172234daedd872a4cd91af66be: $$\int_{0}^{\infty} \frac{e^{-x^2}}{\sqrt{\pi}} \, dx = \frac{1}{2}$$
1519

@@ -50,30 +54,32 @@ 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
7682
77-
LkPh0PjkEIDh+AAzrEgaOEOOXNCAHNEAA===
83+
OvVdNHX7CFR4SD5HIDgOmyzYceRAZAaY1eY3kTGM2P1sICgAr49B6gtyHglIBchSA1QXQVVH0FghzBlg7AZoFwHcA2URJKMJoBVSTQ8gkwPcnAAwFYC9yzguHjr1lDiNGA4wN/vTwfInswgwQbAFIK4ByNBy7tfQHPyXxDtVkWPMznQQMBNR4hiQnIaslCCJQEhhACIVEJmjuDHAzAVQTyFyBDUh8OQIQIc3AAM5YkBocIOOTmggA5oQAA==
7884
```
7985
%%

src/components/mdx/Excalidraw.tsx

Lines changed: 131 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import katexStyles from 'katex/dist/katex.min.css?raw';
88

99
const config = parse(configRaw);
1010

11+
const KATEX_FONT_FIX = `
12+
@font-face { font-family: 'KaTeX_Main'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Main-Regular.woff2') format('woff2'); }
13+
@font-face { font-family: 'KaTeX_Math'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Math-Italic.woff2') format('woff2'); }
14+
@font-face { font-family: 'KaTeX_Size1'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Size1-Regular.woff2') format('woff2'); }
15+
@font-face { font-family: 'KaTeX_Size2'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Size2-Regular.woff2') format('woff2'); }
16+
@font-face { font-family: 'KaTeX_Size3'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Size3-Regular.woff2') format('woff2'); }
17+
@font-face { font-family: 'KaTeX_Size4'; src: url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/fonts/KaTeX_Size4-Regular.woff2') format('woff2'); }
18+
`;
19+
1120
const ResetIcon = () => (
1221
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1322
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
@@ -73,6 +82,8 @@ export function Excalidraw({
7382
const [rawViewBox, setRawViewBox] = useState<number[] | null>(null);
7483
const overviewTargetRef = useRef<number[] | null>(null);
7584
const currentViewBoxRef = useRef<number[]>([0, 0, 100, 100]);
85+
// Store raw HTML strings and data URLs for inlining replacement
86+
const formulaDataRef = useRef<Record<string, { html: string, dataURL: string }>>({});
7687

7788
const requestRef = useRef<number | null>(null);
7889
const transitionRef = useRef<{ start: number[], end: number[], startTime: number, duration: number } | null>(null);
@@ -123,43 +134,44 @@ export function Excalidraw({
123134
// Populate json.files with LaTeX renders if we found any
124135
if (Object.keys(latexMap).length > 0) {
125136
json.files = json.files || {};
137+
formulaDataRef.current = {}; // Reset map
126138
for (const [id, formula] of Object.entries(latexMap)) {
127139
// Find corresponding image element to get its intended size
128140
const el = json.elements?.find((e: any) => e.fileId === id && !e.isDeleted);
129141
if (!el) continue;
130142

131143
try {
132-
const html = katex.renderToString(formula, { displayMode: true, throwOnError: false });
133-
// Create a self-contained SVG with inlined KaTeX CSS
134-
const svgString = `
135-
<svg xmlns="http://www.w3.org/2000/svg" width="${el.width}" height="${el.height}">
136-
<foreignObject width="100%" height="100%">
137-
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; overflow: hidden;">
138-
<style>
139-
${katexStyles}
140-
.katex-display { margin: 0; }
141-
.katex { font-size: 1.1em; }
142-
/* Fallback for mathematical symbols like integrals when KaTeX fonts are not loaded in SVG data URLs */
143-
.katex-mathml { display: none; }
144-
.katex-html { font-family: KaTeX_Main, "Times New Roman", serif; }
145-
.base { font-family: inherit; }
146-
/* Target specific math symbols to use system math fonts if available */
147-
.mop { font-family: "Cambria Math", "STIX Math", "Segoe UI Symbol", "Apple Symbols", serif !important; }
148-
</style>
149-
${html}
150-
</div>
151-
</foreignObject>
152-
</svg>`.trim();
153-
// Use modern TextEncoder instead of deprecated unescape for Base64 encoding
154-
const bytes = new TextEncoder().encode(svgString);
144+
const html = katex.renderToString(formula, { displayMode: false, throwOnError: false });
145+
146+
// Use a simple, valid SVG placeholder to ensure Excalidraw's exportToSvg
147+
// produces an <image> tag that we can later find and replace.
148+
const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${el.width}" height="${el.height}"><rect width="100%" height="100%" fill="none"/></svg>`;
149+
const bytes = new TextEncoder().encode(placeholderSvg);
155150
const binString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
156151
const dataURL = `data:image/svg+xml;base64,${btoa(binString)}`;
152+
153+
// Save full metadata for absolute positioning
154+
formulaDataRef.current[id] = {
155+
html,
156+
dataURL,
157+
x: el.x,
158+
y: el.y,
159+
width: el.width,
160+
height: el.height,
161+
angle: el.angle || 0
162+
};
163+
157164
json.files[id] = {
158165
mimeType: "image/svg+xml",
159166
id,
160167
dataURL,
161168
created: Date.now()
162169
};
170+
171+
// Force status to success so it's rendered by the exporter
172+
if (el.type === "image") {
173+
el.status = "success";
174+
}
163175
} catch (err) {
164176
console.error("KaTeX rendering failed for ID:", id, err);
165177
}
@@ -213,6 +225,10 @@ export function Excalidraw({
213225
if (el.type === "text" && el.fontFamily === 4) {
214226
return { ...el, fontFamily: 1 };
215227
}
228+
// Ensure images have success status for export
229+
if (el.type === "image" && el.fileId && formulaDataRef.current[el.fileId]) {
230+
return { ...el, status: "success" };
231+
}
216232
return el;
217233
});
218234

@@ -230,11 +246,29 @@ export function Excalidraw({
230246
exportPadding: 10,
231247
});
232248

233-
// 2. CSS for the Magic Ball (Physical Object)
249+
// 2. CSS Style injection (Includes Global KaTeX fonts)
234250
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
251+
252+
// Strip internal @font-face rules from KaTeX CSS to avoid 404s on relative paths
253+
const cleanKatexStyles = katexStyles.replace(/@font-face\s*{[^}]*}/g, '');
254+
235255
style.textContent = `
236256
@import url('${config.font4.cssUrl}');
257+
${KATEX_FONT_FIX}
258+
${cleanKatexStyles}
237259
text { font-family: "${config.font4.name}", ${fontFamily}, sans-serif !important; }
260+
261+
/* Custom styles for inlined KaTeX formulas */
262+
.katex-inline-host {
263+
font-family: KaTeX_Main, "Times New Roman", serif !important;
264+
color: #1a1a1a;
265+
display: flex;
266+
align-items: center;
267+
justify-content: center;
268+
}
269+
.katex-display { margin: 0; }
270+
.katex { font-size: 1.15em; line-height: 1.2; }
271+
238272
@keyframes exc-flow-base { from { stroke-dashoffset: 40; } to { stroke-dashoffset: 0; } }
239273
240274
path[stroke="#0000ff"][stroke-dasharray] {
@@ -249,7 +283,79 @@ export function Excalidraw({
249283
`;
250284
svg.prepend(style);
251285

252-
// 3. Post-process: Dynamic Motion Injection (Motion Engine)
286+
// 3. NUCLEAR POSITIONING & CLEANUP
287+
// Excalidraw's SVG export uses these offsets
288+
const offsetX = minX;
289+
const offsetY = minY;
290+
const PADDING = 10;
291+
292+
// Move through formulas and place them at absolute coordinates in SVG root
293+
activeElements.forEach(el => {
294+
if (el.type === "image" && el.fileId && formulaDataRef.current[el.fileId]) {
295+
const matched = formulaDataRef.current[el.fileId];
296+
297+
// Calculate position relative to SVG viewBox
298+
const targetX = el.x - offsetX + PADDING;
299+
const targetY = el.y - offsetY + PADDING;
300+
301+
const fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
302+
fo.setAttribute("x", targetX.toString());
303+
fo.setAttribute("y", targetY.toString());
304+
fo.setAttribute("width", el.width.toString());
305+
fo.setAttribute("height", el.height.toString());
306+
fo.setAttribute("overflow", "visible");
307+
308+
// Handle rotation if any
309+
if (el.angle !== 0) {
310+
const deg = (el.angle * 180) / Math.PI;
311+
const cx = targetX + el.width / 2;
312+
const cy = targetY + el.height / 2;
313+
fo.setAttribute("transform", `rotate(${deg} ${cx} ${cy})`);
314+
}
315+
316+
const div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
317+
div.className = "katex-inline-host";
318+
(div as any).style.cssText = `
319+
width: 100%; height: 100%;
320+
display: flex; align-items: center; justify-content: center;
321+
overflow: visible; color: #1a1a1a;
322+
font-weight: normal;
323+
`;
324+
div.innerHTML = matched.html;
325+
fo.appendChild(div);
326+
327+
// Append to end of SVG to be on top of everything
328+
svg.appendChild(fo);
329+
}
330+
});
331+
332+
// Clean up original tags that might be confusing or covering
333+
svg.querySelectorAll('image').forEach(img => {
334+
const href = (img.getAttribute('xlink:href') || img.getAttribute('href') || "").trim();
335+
if (Object.values(formulaDataRef.current).some(f => f.dataURL === href)) {
336+
img.remove();
337+
}
338+
});
339+
340+
svg.querySelectorAll('rect').forEach((rect: any) => {
341+
const stroke = rect.getAttribute('stroke');
342+
// Protect Excalidraw frames: frames usually have fill="none" and a name or specific classes
343+
// We only want to remove the specific placeholder boxes that match formula dimensions
344+
const isFrame = rect.hasAttribute('aria-label') || rect.classList.contains('excalidraw-frame');
345+
346+
if (!isFrame && (stroke === "#bbb" || stroke === "#cccccc")) {
347+
// Check if this rect matches any of our formula dimensions to be safe
348+
const w = parseFloat(rect.getAttribute('width') || "0");
349+
const h = parseFloat(rect.getAttribute('height') || "0");
350+
const isMatch = Object.values(formulaDataRef.current).some(f =>
351+
Math.abs(f.width - w) < 1 && Math.abs(f.height - h) < 1
352+
);
353+
if (isMatch) rect.remove();
354+
}
355+
});
356+
357+
358+
// 4. Post-process: Dynamic Motion Injection (Motion Engine)
253359
const clusters: { length: number, firstPoint: string }[] = [];
254360
const allPaths = svg.querySelectorAll('path[stroke="#0000ff"]');
255361

0 commit comments

Comments
 (0)