← back to Exo
Enhance LaTeX rendering in dashboard markdown (#1197)
346b13e2c9fdafb85d322f7b48ff6fa46548add0 · 2026-01-19 14:50:41 +0000 · Alex Cheema
## Motivation
When models output LaTeX-formatted math proofs, the dashboard was not
rendering them correctly. Issues included:
- `\documentclass`, `\begin{document}`, `\usepackage` showing as raw
text
- `$...$` inline math with complex expressions (like `\frac`, `\ldots`)
not rendering due to markdown escaping backslashes
- `\begin{align*}...\end{align*}` and other math environments showing as
raw text
- `\emph{...}`, `\textbf{...}` LaTeX formatting commands not being
converted
- `$\require{...}$` (MathJax-specific) causing KaTeX errors
- `\begin{proof}...\end{proof}` showing as raw text
## Changes
Enhanced `MarkdownContent.svelte` with comprehensive LaTeX support:
**Math extraction before markdown processing:**
- Extract `$...$`, `$$...$$`, `\(...\)`, `\[...\]` into placeholders
before markdown processes the text
- Use alphanumeric placeholders (`MATHPLACEHOLDERINLINE0END`) that won't
be interpreted as HTML tags
- Restore and render with KaTeX after markdown processing
**LaTeX document command removal:**
- Strip `\documentclass{...}`, `\usepackage{...}`, `\begin{document}`,
`\end{document}`
- Strip `\maketitle`, `\title{...}`, `\author{...}`, `\date{...}`
- Strip `\require{...}` (MathJax-specific, not KaTeX)
- Replace `tikzpicture` environments with `[diagram]` placeholder
- Strip `\label{...}` cross-reference commands
**LaTeX math environments:**
- Convert `\begin{align*}`, `\begin{equation}`, `\begin{gather}`, etc.
to display math blocks
**LaTeX text formatting:**
- `\emph{...}` and `\textit{...}` → `<em>...</em>`
- `\textbf{...}` → `<strong>...</strong>`
- `\texttt{...}` → `<code>...</code>`
- `\underline{...}` → `<u>...</u>`
**LaTeX environments styling:**
- `\begin{proof}...\end{proof}` → styled proof block with QED symbol
- `\begin{theorem}`, `\begin{lemma}`, etc. → styled theorem blocks
**Display math enhancements:**
- Wrapped in styled container with subtle gold border
- "LaTeX" label and copy button appear on hover
- Dark theme KaTeX color overrides for better readability
- Custom scrollbar for overflow
## Why It Works
The key insight is that markdown processing was escaping backslashes in
LaTeX before KaTeX could see them. By extracting all math expressions
into alphanumeric placeholders *before* markdown runs, then restoring
them *after*, the LaTeX content passes through to KaTeX unmodified.
Using purely alphanumeric placeholders like `MATHPLACEHOLDERINLINE0END`
instead of `<<MATH_INLINE_0>>` prevents markdown from interpreting them
as HTML tags and stripping them.
## Test Plan
### Manual Testing
- Hardware: Any machine with the dashboard
- What you did:
- Ask model to "write a proof in latex"
- Verify inline math like `$x \in S$` renders correctly
- Verify display math like `\begin{align*}...\end{align*}` renders as
block
- Verify `\documentclass`, `\begin{document}` are stripped (not shown)
- Verify `\emph{...}` converts to italics
- Verify copy button works on display math blocks
- Test edge cases: `$5` (currency) stays as text, `\$50` (escaped)
becomes `$50`
Before:
<img width="799" height="637" alt="Screenshot 2026-01-19 at 11 51 22 AM"
src="https://github.com/user-attachments/assets/62a705b8-b3c2-47b8-afd0-5d0c1b240e44"
/>
After:
<img width="809" height="642" alt="Screenshot 2026-01-19 at 11 46 58 AM"
src="https://github.com/user-attachments/assets/4f35fa1d-333c-4285-bc68-58a50f8f148e"
/>
### Automated Testing
- Dashboard builds successfully with `npm run build`
- Existing functionality preserved
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Files touched
M dashboard/src/lib/components/MarkdownContent.svelte
Diff
commit 346b13e2c9fdafb85d322f7b48ff6fa46548add0
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Mon Jan 19 14:50:41 2026 +0000
Enhance LaTeX rendering in dashboard markdown (#1197)
## Motivation
When models output LaTeX-formatted math proofs, the dashboard was not
rendering them correctly. Issues included:
- `\documentclass`, `\begin{document}`, `\usepackage` showing as raw
text
- `$...$` inline math with complex expressions (like `\frac`, `\ldots`)
not rendering due to markdown escaping backslashes
- `\begin{align*}...\end{align*}` and other math environments showing as
raw text
- `\emph{...}`, `\textbf{...}` LaTeX formatting commands not being
converted
- `$\require{...}$` (MathJax-specific) causing KaTeX errors
- `\begin{proof}...\end{proof}` showing as raw text
## Changes
Enhanced `MarkdownContent.svelte` with comprehensive LaTeX support:
**Math extraction before markdown processing:**
- Extract `$...$`, `$$...$$`, `\(...\)`, `\[...\]` into placeholders
before markdown processes the text
- Use alphanumeric placeholders (`MATHPLACEHOLDERINLINE0END`) that won't
be interpreted as HTML tags
- Restore and render with KaTeX after markdown processing
**LaTeX document command removal:**
- Strip `\documentclass{...}`, `\usepackage{...}`, `\begin{document}`,
`\end{document}`
- Strip `\maketitle`, `\title{...}`, `\author{...}`, `\date{...}`
- Strip `\require{...}` (MathJax-specific, not KaTeX)
- Replace `tikzpicture` environments with `[diagram]` placeholder
- Strip `\label{...}` cross-reference commands
**LaTeX math environments:**
- Convert `\begin{align*}`, `\begin{equation}`, `\begin{gather}`, etc.
to display math blocks
**LaTeX text formatting:**
- `\emph{...}` and `\textit{...}` → `<em>...</em>`
- `\textbf{...}` → `<strong>...</strong>`
- `\texttt{...}` → `<code>...</code>`
- `\underline{...}` → `<u>...</u>`
**LaTeX environments styling:**
- `\begin{proof}...\end{proof}` → styled proof block with QED symbol
- `\begin{theorem}`, `\begin{lemma}`, etc. → styled theorem blocks
**Display math enhancements:**
- Wrapped in styled container with subtle gold border
- "LaTeX" label and copy button appear on hover
- Dark theme KaTeX color overrides for better readability
- Custom scrollbar for overflow
## Why It Works
The key insight is that markdown processing was escaping backslashes in
LaTeX before KaTeX could see them. By extracting all math expressions
into alphanumeric placeholders *before* markdown runs, then restoring
them *after*, the LaTeX content passes through to KaTeX unmodified.
Using purely alphanumeric placeholders like `MATHPLACEHOLDERINLINE0END`
instead of `<<MATH_INLINE_0>>` prevents markdown from interpreting them
as HTML tags and stripping them.
## Test Plan
### Manual Testing
- Hardware: Any machine with the dashboard
- What you did:
- Ask model to "write a proof in latex"
- Verify inline math like `$x \in S$` renders correctly
- Verify display math like `\begin{align*}...\end{align*}` renders as
block
- Verify `\documentclass`, `\begin{document}` are stripped (not shown)
- Verify `\emph{...}` converts to italics
- Verify copy button works on display math blocks
- Test edge cases: `$5` (currency) stays as text, `\$50` (escaped)
becomes `$50`
Before:
<img width="799" height="637" alt="Screenshot 2026-01-19 at 11 51 22 AM"
src="https://github.com/user-attachments/assets/62a705b8-b3c2-47b8-afd0-5d0c1b240e44"
/>
After:
<img width="809" height="642" alt="Screenshot 2026-01-19 at 11 46 58 AM"
src="https://github.com/user-attachments/assets/4f35fa1d-333c-4285-bc68-58a50f8f148e"
/>
### Automated Testing
- Dashboard builds successfully with `npm run build`
- Existing functionality preserved
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
---
.../src/lib/components/MarkdownContent.svelte | 611 +++++++++++++++++++--
1 file changed, 565 insertions(+), 46 deletions(-)
diff --git a/dashboard/src/lib/components/MarkdownContent.svelte b/dashboard/src/lib/components/MarkdownContent.svelte
index e30da611..7ac3b66b 100644
--- a/dashboard/src/lib/components/MarkdownContent.svelte
+++ b/dashboard/src/lib/components/MarkdownContent.svelte
@@ -53,62 +53,285 @@
marked.use({ renderer });
/**
- * Preprocess LaTeX: convert \(...\) to $...$ and \[...\] to $$...$$
- * Also protect code blocks from LaTeX processing
+ * Unescape HTML entities that marked may have escaped
+ */
+ function unescapeHtmlEntities(text: string): string {
+ return text
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'");
+ }
+
+ // Storage for math expressions extracted before markdown processing
+ const mathExpressions: Map<string, { content: string; displayMode: boolean }> = new Map();
+ let mathCounter = 0;
+
+ // Storage for HTML snippets that need protection from markdown
+ const htmlSnippets: Map<string, string> = new Map();
+ let htmlCounter = 0;
+
+ // Use alphanumeric placeholders that won't be interpreted as HTML tags
+ const MATH_PLACEHOLDER_PREFIX = 'MATHPLACEHOLDER';
+ const CODE_PLACEHOLDER_PREFIX = 'CODEPLACEHOLDER';
+ const HTML_PLACEHOLDER_PREFIX = 'HTMLPLACEHOLDER';
+
+ /**
+ * Preprocess LaTeX: extract math, handle LaTeX document commands, and protect content
*/
function preprocessLaTeX(text: string): string {
- // Protect code blocks
+ // Reset storage
+ mathExpressions.clear();
+ mathCounter = 0;
+ htmlSnippets.clear();
+ htmlCounter = 0;
+
+ // Protect code blocks first
const codeBlocks: string[] = [];
let processed = text.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => {
codeBlocks.push(match);
- return `<<CODE_${codeBlocks.length - 1}>>`;
+ return `${CODE_PLACEHOLDER_PREFIX}${codeBlocks.length - 1}END`;
+ });
+
+ // Remove LaTeX document commands
+ processed = processed.replace(/\\documentclass(\[[^\]]*\])?\{[^}]*\}/g, '');
+ processed = processed.replace(/\\usepackage(\[[^\]]*\])?\{[^}]*\}/g, '');
+ processed = processed.replace(/\\begin\{document\}/g, '');
+ processed = processed.replace(/\\end\{document\}/g, '');
+ processed = processed.replace(/\\maketitle/g, '');
+ processed = processed.replace(/\\title\{[^}]*\}/g, '');
+ processed = processed.replace(/\\author\{[^}]*\}/g, '');
+ processed = processed.replace(/\\date\{[^}]*\}/g, '');
+
+ // Remove \require{...} commands (MathJax-specific, not supported by KaTeX)
+ processed = processed.replace(/\$\\require\{[^}]*\}\$/g, '');
+ processed = processed.replace(/\\require\{[^}]*\}/g, '');
+
+ // Remove unsupported LaTeX commands/environments (tikzpicture, figure, center, etc.)
+ processed = processed.replace(/\\begin\{tikzpicture\}[\s\S]*?\\end\{tikzpicture\}/g, () => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, '<div class="latex-diagram-placeholder"><span class="latex-diagram-icon">📐</span><span class="latex-diagram-text">Diagram</span></div>');
+ htmlCounter++;
+ return placeholder;
+ });
+ processed = processed.replace(/\\begin\{figure\}[\s\S]*?\\end\{figure\}/g, () => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, '<div class="latex-diagram-placeholder"><span class="latex-diagram-icon">🖼️</span><span class="latex-diagram-text">Figure</span></div>');
+ htmlCounter++;
+ return placeholder;
+ });
+ // Strip center environment (layout only, no content change)
+ processed = processed.replace(/\\begin\{center\}/g, '');
+ processed = processed.replace(/\\end\{center\}/g, '');
+ // Strip other layout environments
+ processed = processed.replace(/\\begin\{flushleft\}/g, '');
+ processed = processed.replace(/\\end\{flushleft\}/g, '');
+ processed = processed.replace(/\\begin\{flushright\}/g, '');
+ processed = processed.replace(/\\end\{flushright\}/g, '');
+ processed = processed.replace(/\\label\{[^}]*\}/g, '');
+ processed = processed.replace(/\\caption\{[^}]*\}/g, '');
+
+ // Protect escaped dollar signs (e.g., \$50 should become $50, not LaTeX)
+ processed = processed.replace(/\\\$/g, 'ESCAPEDDOLLARPLACEHOLDER');
+
+ // Convert LaTeX math environments to display math (both bare and wrapped in $...$)
+ const mathEnvs = ['align', 'align\\*', 'equation', 'equation\\*', 'gather', 'gather\\*', 'multline', 'multline\\*', 'eqnarray', 'eqnarray\\*', 'array', 'matrix', 'pmatrix', 'bmatrix', 'vmatrix', 'cases'];
+ for (const env of mathEnvs) {
+ // Handle $\begin{env}...\end{env}$ (with dollar signs, possibly multiline)
+ const wrappedRegex = new RegExp(`\\$\\\\begin\\{${env}\\}(\\{[^}]*\\})?([\\s\\S]*?)\\\\end\\{${env}\\}\\$`, 'g');
+ processed = processed.replace(wrappedRegex, (_, args, content) => {
+ const cleanEnv = env.replace('\\*', '*');
+ const mathContent = `\\begin{${cleanEnv}}${args || ''}${content}\\end{${cleanEnv}}`;
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}DISPLAY${mathCounter}END`;
+ mathExpressions.set(placeholder, { content: mathContent, displayMode: true });
+ mathCounter++;
+ return placeholder;
+ });
+
+ // Handle bare \begin{env}...\end{env} (without dollar signs)
+ const bareRegex = new RegExp(`\\\\begin\\{${env}\\}(\\{[^}]*\\})?([\\s\\S]*?)\\\\end\\{${env}\\}`, 'g');
+ processed = processed.replace(bareRegex, (_, args, content) => {
+ const cleanEnv = env.replace('\\*', '*');
+ const mathContent = `\\begin{${cleanEnv}}${args || ''}${content}\\end{${cleanEnv}}`;
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}DISPLAY${mathCounter}END`;
+ mathExpressions.set(placeholder, { content: mathContent, displayMode: true });
+ mathCounter++;
+ return placeholder;
+ });
+ }
+
+ // Convert LaTeX proof environments to styled blocks (use placeholders for HTML)
+ processed = processed.replace(
+ /\\begin\{proof\}([\s\S]*?)\\end\{proof\}/g,
+ (_, content) => {
+ const html = `<div class="latex-proof"><div class="latex-proof-header">Proof</div><div class="latex-proof-content">${content}</div></div>`;
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, html);
+ htmlCounter++;
+ return placeholder;
+ }
+ );
+
+ // Convert LaTeX theorem-like environments
+ const theoremEnvs = ['theorem', 'lemma', 'corollary', 'proposition', 'definition', 'remark', 'example'];
+ for (const env of theoremEnvs) {
+ const envRegex = new RegExp(`\\\\begin\\{${env}\\}([\\s\\S]*?)\\\\end\\{${env}\\}`, 'gi');
+ const envName = env.charAt(0).toUpperCase() + env.slice(1);
+ processed = processed.replace(envRegex, (_, content) => {
+ const html = `<div class="latex-theorem"><div class="latex-theorem-header">${envName}</div><div class="latex-theorem-content">${content}</div></div>`;
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, html);
+ htmlCounter++;
+ return placeholder;
+ });
+ }
+
+ // Convert LaTeX text formatting commands (use placeholders to protect from markdown)
+ processed = processed.replace(/\\emph\{([^}]*)\}/g, (_, content) => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, `<em>${content}</em>`);
+ htmlCounter++;
+ return placeholder;
+ });
+ processed = processed.replace(/\\textit\{([^}]*)\}/g, (_, content) => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, `<em>${content}</em>`);
+ htmlCounter++;
+ return placeholder;
+ });
+ processed = processed.replace(/\\textbf\{([^}]*)\}/g, (_, content) => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, `<strong>${content}</strong>`);
+ htmlCounter++;
+ return placeholder;
+ });
+ processed = processed.replace(/\\texttt\{([^}]*)\}/g, (_, content) => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, `<code class="inline-code">${content}</code>`);
+ htmlCounter++;
+ return placeholder;
+ });
+ processed = processed.replace(/\\underline\{([^}]*)\}/g, (_, content) => {
+ const placeholder = `${HTML_PLACEHOLDER_PREFIX}${htmlCounter}END`;
+ htmlSnippets.set(placeholder, `<u>${content}</u>`);
+ htmlCounter++;
+ return placeholder;
+ });
+
+ // Handle LaTeX line breaks and spacing
+ processed = processed.replace(/\\\\(?:\s*\n)?/g, '\n'); // \\ -> newline
+ processed = processed.replace(/\\newline/g, '\n');
+ processed = processed.replace(/\\par\b/g, '\n\n');
+ processed = processed.replace(/\\quad/g, ' ');
+ processed = processed.replace(/\\qquad/g, ' ');
+ processed = processed.replace(/~~/g, ' '); // non-breaking space
+
+ // Remove other common LaTeX commands that don't render
+ processed = processed.replace(/\\centering/g, '');
+ processed = processed.replace(/\\noindent/g, '');
+ processed = processed.replace(/\\hfill/g, '');
+ processed = processed.replace(/\\vspace\{[^}]*\}/g, '');
+ processed = processed.replace(/\\hspace\{[^}]*\}/g, ' ');
+
+ // Convert \(...\) to placeholder (display: false)
+ processed = processed.replace(/\\\(([\s\S]+?)\\\)/g, (_, content) => {
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}INLINE${mathCounter}END`;
+ mathExpressions.set(placeholder, { content, displayMode: false });
+ mathCounter++;
+ return placeholder;
});
- // Convert \(...\) to $...$
- processed = processed.replace(/\\\((.+?)\\\)/g, '$$$1$');
-
- // Convert \[...\] to $$...$$
- processed = processed.replace(/\\\[([\s\S]*?)\\\]/g, '$$$$$1$$$$');
+ // Convert \[...\] to placeholder (display: true)
+ processed = processed.replace(/\\\[([\s\S]*?)\\\]/g, (_, content) => {
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}DISPLAY${mathCounter}END`;
+ mathExpressions.set(placeholder, { content, displayMode: true });
+ mathCounter++;
+ return placeholder;
+ });
+
+ // Extract display math ($$...$$) BEFORE markdown processing
+ processed = processed.replace(/\$\$([\s\S]*?)\$\$/g, (_, content) => {
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}DISPLAY${mathCounter}END`;
+ mathExpressions.set(placeholder, { content: content.trim(), displayMode: true });
+ mathCounter++;
+ return placeholder;
+ });
+
+ // Extract inline math ($...$) BEFORE markdown processing
+ // Allow single-line only, skip currency patterns like $5 or $50
+ processed = processed.replace(/\$([^\$\n]+?)\$/g, (match, content) => {
+ if (/^\d/.test(content.trim())) {
+ return match; // Keep as-is for currency
+ }
+ const placeholder = `${MATH_PLACEHOLDER_PREFIX}INLINE${mathCounter}END`;
+ mathExpressions.set(placeholder, { content: content.trim(), displayMode: false });
+ mathCounter++;
+ return placeholder;
+ });
+
+ // Restore escaped dollar signs
+ processed = processed.replace(/ESCAPEDDOLLARPLACEHOLDER/g, '$');
// Restore code blocks
- processed = processed.replace(/<<CODE_(\d+)>>/g, (_, index) => codeBlocks[parseInt(index)]);
+ processed = processed.replace(new RegExp(`${CODE_PLACEHOLDER_PREFIX}(\\d+)END`, 'g'), (_, index) => codeBlocks[parseInt(index)]);
+
+ // Clean up any remaining stray backslashes from unrecognized commands
+ processed = processed.replace(/\\(?=[a-zA-Z])/g, ''); // Remove \ before letters (unrecognized commands)
return processed;
}
/**
- * Render math expressions with KaTeX after HTML is generated
+ * Render math expressions with KaTeX and restore HTML placeholders
*/
function renderMath(html: string): string {
- // Render display math ($$...$$)
- html = html.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
- try {
- return katex.renderToString(math.trim(), {
- displayMode: true,
- throwOnError: false,
- output: 'html'
- });
- } catch {
- return `<span class="math-error">$$${math}$$</span>`;
- }
- });
+ // Replace all math placeholders with rendered KaTeX
+ for (const [placeholder, { content, displayMode }] of mathExpressions) {
+ const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const regex = new RegExp(escapedPlaceholder, 'g');
+
+ html = html.replace(regex, () => {
+ try {
+ const rendered = katex.renderToString(content, {
+ displayMode,
+ throwOnError: false,
+ output: 'html'
+ });
+
+ if (displayMode) {
+ return `
+ <div class="math-display-wrapper">
+ <div class="math-display-header">
+ <span class="math-label">LaTeX</span>
+ <button type="button" class="copy-math-btn" data-math-source="${encodeURIComponent(content)}" title="Copy LaTeX source">
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
+ </svg>
+ </button>
+ </div>
+ <div class="math-display-content">
+ ${rendered}
+ </div>
+ </div>
+ `;
+ } else {
+ return `<span class="math-inline">${rendered}</span>`;
+ }
+ } catch {
+ const display = displayMode ? `$$${content}$$` : `$${content}$`;
+ return `<span class="math-error"><span class="math-error-icon">⚠</span> ${display}</span>`;
+ }
+ });
+ }
- // Render inline math ($...$) but avoid matching currency like $5
- html = html.replace(/\$([^\$\n]+?)\$/g, (match, math) => {
- // Skip if it looks like currency ($ followed by number)
- if (/^\d/.test(math.trim())) {
- return match;
- }
- try {
- return katex.renderToString(math.trim(), {
- displayMode: false,
- throwOnError: false,
- output: 'html'
- });
- } catch {
- return `<span class="math-error">$${math}$</span>`;
- }
- });
+ // Restore HTML placeholders (for \textbf, \emph, etc.)
+ for (const [placeholder, htmlContent] of htmlSnippets) {
+ const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const regex = new RegExp(escapedPlaceholder, 'g');
+ html = html.replace(regex, htmlContent);
+ }
return html;
}
@@ -154,16 +377,50 @@
}
}
+ async function handleMathCopyClick(event: Event) {
+ const target = event.currentTarget as HTMLButtonElement;
+ const encodedSource = target.getAttribute('data-math-source');
+ if (!encodedSource) return;
+
+ const source = decodeURIComponent(encodedSource);
+
+ try {
+ await navigator.clipboard.writeText(source);
+ // Show copied feedback
+ const originalHtml = target.innerHTML;
+ target.innerHTML = `
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M20 6L9 17l-5-5"/>
+ </svg>
+ `;
+ target.classList.add('copied');
+ setTimeout(() => {
+ target.innerHTML = originalHtml;
+ target.classList.remove('copied');
+ }, 2000);
+ } catch (error) {
+ console.error('Failed to copy math:', error);
+ }
+ }
+
function setupCopyButtons() {
if (!containerRef || !browser) return;
- const buttons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn');
- for (const button of buttons) {
+ const codeButtons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn');
+ for (const button of codeButtons) {
if (button.dataset.listenerBound !== 'true') {
button.dataset.listenerBound = 'true';
button.addEventListener('click', handleCopyClick);
}
}
+
+ const mathButtons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-math-btn');
+ for (const button of mathButtons) {
+ if (button.dataset.listenerBound !== 'true') {
+ button.dataset.listenerBound = 'true';
+ button.addEventListener('click', handleMathCopyClick);
+ }
+ }
}
$effect(() => {
@@ -424,28 +681,290 @@
color: #60a5fa;
}
- /* KaTeX math styling */
+ /* KaTeX math styling - Base */
.markdown-content :global(.katex) {
font-size: 1.1em;
+ color: oklch(0.9 0 0);
}
- .markdown-content :global(.katex-display) {
+ /* Display math container wrapper */
+ .markdown-content :global(.math-display-wrapper) {
margin: 1rem 0;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ border: 1px solid rgba(255, 215, 0, 0.15);
+ background: rgba(0, 0, 0, 0.3);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .markdown-content :global(.math-display-wrapper:hover) {
+ border-color: rgba(255, 215, 0, 0.25);
+ box-shadow: 0 0 12px rgba(255, 215, 0, 0.08);
+ }
+
+ /* Display math header - hidden by default, slides in on hover */
+ .markdown-content :global(.math-display-header) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.375rem 0.75rem;
+ background: rgba(255, 215, 0, 0.03);
+ border-bottom: 1px solid rgba(255, 215, 0, 0.08);
+ opacity: 0;
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ overflow: hidden;
+ transition:
+ opacity 0.2s ease,
+ max-height 0.2s ease,
+ padding 0.2s ease;
+ }
+
+ .markdown-content :global(.math-display-wrapper:hover .math-display-header) {
+ opacity: 1;
+ max-height: 2.5rem;
+ padding: 0.375rem 0.75rem;
+ }
+
+ .markdown-content :global(.math-label) {
+ color: rgba(255, 215, 0, 0.7);
+ font-size: 0.65rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, monospace;
+ }
+
+ .markdown-content :global(.copy-math-btn) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.25rem;
+ background: transparent;
+ border: none;
+ color: var(--exo-light-gray, #9ca3af);
+ cursor: pointer;
+ transition: color 0.2s;
+ border-radius: 0.25rem;
+ opacity: 0;
+ transition:
+ color 0.2s,
+ opacity 0.15s ease;
+ }
+
+ .markdown-content :global(.math-display-wrapper:hover .copy-math-btn) {
+ opacity: 1;
+ }
+
+ .markdown-content :global(.copy-math-btn:hover) {
+ color: var(--exo-yellow, #ffd700);
+ }
+
+ .markdown-content :global(.copy-math-btn.copied) {
+ color: #22c55e;
+ }
+
+ /* Display math content area */
+ .markdown-content :global(.math-display-content) {
+ padding: 1rem 1.25rem;
overflow-x: auto;
overflow-y: hidden;
- padding: 0.5rem 0;
}
- .markdown-content :global(.katex-display > .katex) {
+ /* Custom scrollbar for math overflow */
+ .markdown-content :global(.math-display-content::-webkit-scrollbar) {
+ height: 6px;
+ }
+
+ .markdown-content :global(.math-display-content::-webkit-scrollbar-track) {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 3px;
+ }
+
+ .markdown-content :global(.math-display-content::-webkit-scrollbar-thumb) {
+ background: rgba(255, 215, 0, 0.2);
+ border-radius: 3px;
+ }
+
+ .markdown-content :global(.math-display-content::-webkit-scrollbar-thumb:hover) {
+ background: rgba(255, 215, 0, 0.35);
+ }
+
+ .markdown-content :global(.math-display-content .katex-display) {
+ margin: 0;
+ padding: 0;
+ }
+
+ .markdown-content :global(.math-display-content .katex-display > .katex) {
text-align: center;
}
+ /* Inline math wrapper */
+ .markdown-content :global(.math-inline) {
+ display: inline;
+ padding: 0 0.125rem;
+ border-radius: 0.25rem;
+ transition: background-color 0.15s ease;
+ }
+
+ .markdown-content :global(.math-inline:hover) {
+ background: rgba(255, 215, 0, 0.05);
+ }
+
+ /* Dark theme KaTeX overrides */
+ .markdown-content :global(.katex .mord),
+ .markdown-content :global(.katex .minner),
+ .markdown-content :global(.katex .mop),
+ .markdown-content :global(.katex .mbin),
+ .markdown-content :global(.katex .mrel),
+ .markdown-content :global(.katex .mpunct) {
+ color: oklch(0.9 0 0);
+ }
+
+ /* Fraction lines and rules */
+ .markdown-content :global(.katex .frac-line),
+ .markdown-content :global(.katex .overline-line),
+ .markdown-content :global(.katex .underline-line),
+ .markdown-content :global(.katex .hline),
+ .markdown-content :global(.katex .rule) {
+ border-color: oklch(0.85 0 0) !important;
+ background: oklch(0.85 0 0);
+ }
+
+ /* Square roots and SVG elements */
+ .markdown-content :global(.katex .sqrt-line) {
+ border-color: oklch(0.85 0 0) !important;
+ }
+
+ .markdown-content :global(.katex svg) {
+ fill: oklch(0.85 0 0);
+ stroke: oklch(0.85 0 0);
+ }
+
+ .markdown-content :global(.katex svg path) {
+ stroke: oklch(0.85 0 0);
+ }
+
+ /* Delimiters (parentheses, brackets, braces) */
+ .markdown-content :global(.katex .delimsizing),
+ .markdown-content :global(.katex .delim-size1),
+ .markdown-content :global(.katex .delim-size2),
+ .markdown-content :global(.katex .delim-size3),
+ .markdown-content :global(.katex .delim-size4),
+ .markdown-content :global(.katex .mopen),
+ .markdown-content :global(.katex .mclose) {
+ color: oklch(0.75 0 0);
+ }
+
+ /* Math error styling */
.markdown-content :global(.math-error) {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
color: #f87171;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, monospace;
font-size: 0.875em;
background: rgba(248, 113, 113, 0.1);
- padding: 0.125rem 0.25rem;
+ padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ }
+
+ .markdown-content :global(.math-error-icon) {
+ font-size: 0.875em;
+ opacity: 0.9;
+ }
+
+ /* LaTeX proof environment */
+ .markdown-content :global(.latex-proof) {
+ margin: 1rem 0;
+ padding: 1rem 1.25rem;
+ background: rgba(255, 255, 255, 0.02);
+ border-left: 3px solid rgba(255, 215, 0, 0.4);
+ border-radius: 0 0.375rem 0.375rem 0;
+ }
+
+ .markdown-content :global(.latex-proof-header) {
+ font-weight: 600;
+ font-style: italic;
+ color: oklch(0.85 0 0);
+ margin-bottom: 0.5rem;
+ }
+
+ .markdown-content :global(.latex-proof-header::after) {
+ content: '.';
+ }
+
+ .markdown-content :global(.latex-proof-content) {
+ color: oklch(0.9 0 0);
+ }
+
+ .markdown-content :global(.latex-proof-content p:last-child) {
+ margin-bottom: 0;
+ }
+
+ /* QED symbol at end of proof */
+ .markdown-content :global(.latex-proof-content::after) {
+ content: '∎';
+ display: block;
+ text-align: right;
+ color: oklch(0.7 0 0);
+ margin-top: 0.5rem;
+ }
+
+ /* LaTeX theorem-like environments */
+ .markdown-content :global(.latex-theorem) {
+ margin: 1rem 0;
+ padding: 1rem 1.25rem;
+ background: rgba(255, 215, 0, 0.03);
+ border: 1px solid rgba(255, 215, 0, 0.15);
+ border-radius: 0.375rem;
+ }
+
+ .markdown-content :global(.latex-theorem-header) {
+ font-weight: 700;
+ color: var(--exo-yellow, #ffd700);
+ margin-bottom: 0.5rem;
+ }
+
+ .markdown-content :global(.latex-theorem-header::after) {
+ content: '.';
+ }
+
+ .markdown-content :global(.latex-theorem-content) {
+ color: oklch(0.9 0 0);
+ font-style: italic;
+ }
+
+ .markdown-content :global(.latex-theorem-content p:last-child) {
+ margin-bottom: 0;
+ }
+
+ /* LaTeX diagram/figure placeholder */
+ .markdown-content :global(.latex-diagram-placeholder) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin: 1rem 0;
+ padding: 1.5rem 2rem;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px dashed rgba(255, 215, 0, 0.25);
+ border-radius: 0.5rem;
+ color: rgba(255, 215, 0, 0.6);
+ font-size: 0.875rem;
+ }
+
+ .markdown-content :global(.latex-diagram-icon) {
+ font-size: 1.25rem;
+ opacity: 0.8;
+ }
+
+ .markdown-content :global(.latex-diagram-text) {
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, monospace;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
}
</style>
← ea058842 Custom mlx layer composition (#1201)
·
back to Exo
·
shard_downloader: make on_progress callback async fb015163 →