|
|
|
@@ -53,188 +53,62 @@
|
|
|
|
|
marked.use({ renderer });
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
|
|
// Use alphanumeric placeholders that won't be interpreted as HTML tags
|
|
|
|
|
const MATH_PLACEHOLDER_PREFIX = 'MATHPLACEHOLDER';
|
|
|
|
|
const CODE_PLACEHOLDER_PREFIX = 'CODEPLACEHOLDER';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Preprocess LaTeX: extract math, handle LaTeX document commands, and protect content
|
|
|
|
|
* Preprocess LaTeX: convert \(...\) to $...$ and \[...\] to $$...$$
|
|
|
|
|
* Also protect code blocks from LaTeX processing
|
|
|
|
|
*/
|
|
|
|
|
function preprocessLaTeX(text: string): string {
|
|
|
|
|
// Reset math storage
|
|
|
|
|
mathExpressions.clear();
|
|
|
|
|
mathCounter = 0;
|
|
|
|
|
|
|
|
|
|
// Protect code blocks first
|
|
|
|
|
// Protect code blocks
|
|
|
|
|
const codeBlocks: string[] = [];
|
|
|
|
|
let processed = text.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => {
|
|
|
|
|
codeBlocks.push(match);
|
|
|
|
|
return `${CODE_PLACEHOLDER_PREFIX}${codeBlocks.length - 1}END`;
|
|
|
|
|
return `<<CODE_${codeBlocks.length - 1}>>`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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, etc.)
|
|
|
|
|
processed = processed.replace(/\\begin\{tikzpicture\}[\s\S]*?\\end\{tikzpicture\}/g, '[diagram]');
|
|
|
|
|
processed = processed.replace(/\\label\{[^}]*\}/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
|
|
|
|
|
const mathEnvs = ['align', 'align\\*', 'equation', 'equation\\*', 'gather', 'gather\\*', 'multline', 'multline\\*', 'eqnarray', 'eqnarray\\*'];
|
|
|
|
|
for (const env of mathEnvs) {
|
|
|
|
|
const envRegex = new RegExp(`\\\\begin\\{${env}\\}([\\s\\S]*?)\\\\end\\{${env}\\}`, 'g');
|
|
|
|
|
processed = processed.replace(envRegex, (_, content) => {
|
|
|
|
|
// For align environments, wrap content properly for KaTeX
|
|
|
|
|
const cleanEnv = env.replace('\\*', '*');
|
|
|
|
|
const mathContent = `\\begin{${cleanEnv}}${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
|
|
|
|
|
processed = processed.replace(
|
|
|
|
|
/\\begin\{proof\}([\s\S]*?)\\end\{proof\}/g,
|
|
|
|
|
'<div class="latex-proof"><div class="latex-proof-header">Proof</div><div class="latex-proof-content">$1</div></div>'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
`<div class="latex-theorem"><div class="latex-theorem-header">${envName}</div><div class="latex-theorem-content">$1</div></div>`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert LaTeX text formatting commands
|
|
|
|
|
processed = processed.replace(/\\emph\{([^}]*)\}/g, '<em>$1</em>');
|
|
|
|
|
processed = processed.replace(/\\textit\{([^}]*)\}/g, '<em>$1</em>');
|
|
|
|
|
processed = processed.replace(/\\textbf\{([^}]*)\}/g, '<strong>$1</strong>');
|
|
|
|
|
processed = processed.replace(/\\texttt\{([^}]*)\}/g, '<code class="inline-code">$1</code>');
|
|
|
|
|
processed = processed.replace(/\\underline\{([^}]*)\}/g, '<u>$1</u>');
|
|
|
|
|
|
|
|
|
|
// Convert \(...\) to placeholder (display: false)
|
|
|
|
|
processed = processed.replace(/\\\((.+?)\\\)/g, (_, content) => {
|
|
|
|
|
const placeholder = `${MATH_PLACEHOLDER_PREFIX}INLINE${mathCounter}END`;
|
|
|
|
|
mathExpressions.set(placeholder, { content, displayMode: false });
|
|
|
|
|
mathCounter++;
|
|
|
|
|
return placeholder;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// 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, '$');
|
|
|
|
|
// Convert \(...\) to $...$
|
|
|
|
|
processed = processed.replace(/\\\((.+?)\\\)/g, '$$$1$');
|
|
|
|
|
|
|
|
|
|
// Convert \[...\] to $$...$$
|
|
|
|
|
processed = processed.replace(/\\\[([\s\S]*?)\\\]/g, '$$$$$1$$$$');
|
|
|
|
|
|
|
|
|
|
// Restore code blocks
|
|
|
|
|
processed = processed.replace(new RegExp(`${CODE_PLACEHOLDER_PREFIX}(\\d+)END`, 'g'), (_, index) => codeBlocks[parseInt(index)]);
|
|
|
|
|
processed = processed.replace(/<<CODE_(\d+)>>/g, (_, index) => codeBlocks[parseInt(index)]);
|
|
|
|
|
|
|
|
|
|
return processed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render math expressions with KaTeX - restores placeholders with rendered math
|
|
|
|
|
* Render math expressions with KaTeX after HTML is generated
|
|
|
|
|
*/
|
|
|
|
|
function renderMath(html: string): string {
|
|
|
|
|
// 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');
|
|
|
|
|
// 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>`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return html;
|
|
|
|
|
}
|
|
|
|
@@ -280,50 +154,16 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 codeButtons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn');
|
|
|
|
|
for (const button of codeButtons) {
|
|
|
|
|
const buttons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn');
|
|
|
|
|
for (const button of buttons) {
|
|
|
|
|
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(() => {
|
|
|
|
@@ -584,263 +424,28 @@
|
|
|
|
|
color: #60a5fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* KaTeX math styling - Base */
|
|
|
|
|
/* KaTeX math styling */
|
|
|
|
|
.markdown-content :global(.katex) {
|
|
|
|
|
font-size: 1.1em;
|
|
|
|
|
color: oklch(0.9 0 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Display math container wrapper */
|
|
|
|
|
.markdown-content :global(.math-display-wrapper) {
|
|
|
|
|
.markdown-content :global(.katex-display) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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) {
|
|
|
|
|
.markdown-content :global(.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.25rem 0.5rem;
|
|
|
|
|
padding: 0.125rem 0.25rem;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|