// Theme handling const themeToggle = document.getElementById('themeToggle'); const themeIcon = document.getElementById('themeIcon'); function applyStoredTheme() { const stored = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const useDark = stored ? stored === 'dark' : prefersDark; document.documentElement.classList.toggle('dark', useDark); themeIcon.textContent = useDark ? 'ā˜€ļø' : 'šŸŒ™'; } applyStoredTheme(); themeToggle.addEventListener('click', () => { const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', isDark ? 'dark' : 'light'); themeIcon.textContent = isDark ? 'ā˜€ļø' : 'šŸŒ™'; }); const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); const renderBtn = document.getElementById('renderBtn'); const copyTexBtn = document.getElementById('copyTexBtn'); const clearBtn = document.getElementById('clearBtn'); const renderError = document.getElementById('renderError'); const autofixStatus = document.getElementById('autofixStatus'); function renderLatex(value) { renderError.classList.add('hidden'); renderError.textContent = ''; preview.innerHTML = ''; try { // Reset content before rendering to ensure size recalculations katex.render(value || '', preview, { throwOnError: true, displayMode: true, trust: false, strict: 'warn', output: 'htmlAndMathml', macros: { '\\f': '#1f(#2)', }, }); } catch (err) { renderError.classList.remove('hidden'); renderError.textContent = 'KaTeX error: ' + (err?.message || String(err)); // Render with throwOnError=false to still show a best-effort preview try { katex.render(value || '', preview, { throwOnError: false, displayMode: true, trust: false, strict: 'warn', output: 'htmlAndMathml', }); } catch (e2) { // ignore } } } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Simple tokenizer to avoid replacing inside macro or environment names function findLikelyCommandRanges(text) { // Return ranges [start,end) of sequences likely to be command names // (alphabetic sequences after a backslash) const ranges = []; const re = /\\([A-Za-z]+)/g; let m; while ((m = re.exec(text)) !== null) { ranges.push([m.index, m.index + m[0].length]); } return ranges; } function isIndexInRanges(index, ranges) { for (const [s, e] of ranges) { if (index >= s && index < e) return true; } return false; } // Auto syntax fixer function autoFixLatex(input) { if (!input) return input; let text = input; // Normalize whitespace and dashes text = text .replace(/\r\n?/g, '\n') .replace(/[ \t]+/g, ' ') .replace(/–|—/g, '-') .replace(/āˆ’/g, '-'); // Convert common ASCII relations to LaTeX const replacements = [ { from: '>=', to: '\\geq' }, { from: '<=', to: '\\leq' }, { from: '!=', to: '\\neq' }, { from: '~=', to: '\\approx' }, { from: '+=+', to: '\\pm' }, // handle user typed "+=+" { from: '+/-', to: '\\pm' }, ]; for (const r of replacements) { const re = new RegExp(escapeRegExp(r.from), 'g'); text = text.replace(re, r.to); } // Convert common words to functions const funcs = ['sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'log', 'ln', 'exp', 'max', 'min', 'argmax', 'argmin']; const ranges = findLikelyCommandRanges(text); for (const fn of funcs) { const re = new RegExp(`\\b${escapeRegExp(fn)}\\b`, 'gi'); text = text.replace(re, (m) => { const idx = m.index ?? 0; // If already preceded by backslash, leave it if (idx > 0 && text[idx - 1] === '\\') return m; // If inside an already detected command, leave it if (isIndexInRanges(idx, ranges)) return m; return `\\${m.toLowerCase()}`; }); } // sqrt shortcuts: "sqrt(...)" or user typed "^/" to suggest root text = text.replace(/\bsqrt\s*\(\s*([^)]+?)\s*\)/gi, (_, inside) => `\\sqrt{${inside}}`); // If user typed "n√" like n√x, convert to \sqrt[n]{x} when braces after caret were intended text = text.replace(/(\d+)\s*\^\s*\/\s*([A-Za-z0-9\\]+)/g, (_, n, expr) => `\\sqrt[${n}]{${expr}}`); // Replace slash "a/b" with fraction if likely math context (alphanumeric on both sides) text = text.replace(/([A-Za-z0-9}\)])\s*\/\s*([A-Za-z0-9{($]|\^|_|\\\\)/g, (m, a, b) => { // Avoid http:// and similar if (/https?:\/\//.test(m)) return m; return `\\frac{${a}}{${b}}`; }); // Ensure some common operators are LaTeX text = text.replace(/\[/g, '\\left[').replace(/\]/g, '\\right]'); text = text.replace(/\(/g, '\\left(').replace(/\)/g, '\\right)'); text = text.replace(/\|/g, '\\left|\\,\\right|'); // moderate auto-grouping // Normalize percentages and degrees text = text.replace(/([A-Za-z0-9\)])\s*%\s*/g, '$1\\%'); // Fix double backslashes and missing backslash in common commands text = text.replace(/\\+/g, (m) => (m.length % 2 === 0 ? '\\' : m)); // keep single backslash // Replace plain asterisks with \cdot in likely math contexts text = text.replace(/([A-Za-z0-9])\s*\*\s*([A-Za-z0-9])/g, '$1\\cdot$2'); return text; } // Insert snippet helpers function insertSnippet(value) { const start = editor.selectionStart; const end = editor.selectionEnd; const before = editor.value.slice(0, start); const after = editor.value.slice(end); // If snippet contains ā– , select the first placeholder region (including braces) so user can type over it let newValue = before + value + after; let cursorPos = newValue.length; const placeholderIdx = value.indexOf('ā– '); if (placeholderIdx !== -1) { // Replace only the first ā–  with empty string; we'll select a region around it to ease typing const cleaned = value.replace('ā– ', ''); const insertPos = before.length + placeholderIdx; // Try to find the nearest pair of braces after the placeholder to select content inside let selectStart = insertPos; let selectEnd = insertPos; // Naive: select a short word after placeholder if exists; else just place cursor const rest = value.slice(placeholderIdx + 1); const matchWord = rest.match(/^[A-Za-z0-9\\^_]+/); if (matchWord) { selectEnd = insertPos + matchWord[0].length; } else { selectStart = insertPos; selectEnd = insertPos; } newValue = before + cleaned + after; editor.value = newValue; // Put cursor or select content requestAnimationFrame(() => { editor.focus(); if (selectEnd > selectStart) { editor.setSelectionRange(selectStart, selectEnd); } else { editor.setSelectionRange(insertPos, insertPos); } // Trigger rendering and fixing triggerUpdate(); }); return; } // No placeholder: just insert editor.value = newValue; requestAnimationFrame(() => { const caret = before.length + value.length; editor.setSelectionRange(caret, caret); editor.focus(); triggerUpdate(); }); } // Command board click handling document.querySelectorAll('.cmd').forEach(btn => { btn.addEventListener('click', () => { const snippet = btn.getAttribute('data-insert') || ''; insertSnippet(snippet); }); }); // Keyboard navigation within command board (Tab/Shift+Tab) document.addEventListener('keydown', (e) => { const isCommandRegion = e.target.closest('.cmd') !== null; if (!isCommandRegion) return; const cmds = Array.from(document.querySelectorAll('.cmd')); const idx = cmds.indexOf(e.target); if (e.key === 'Tab') { e.preventDefault(); const dir = e.shiftKey ? -1 : 1; const next = (idx + dir + cmds.length) % cmds.length; cmds[next].focus(); } }); // Update pipeline let lastRendered = ''; let lastFixed = ''; let lastUserInput = ''; function triggerUpdate() { const current = editor.value; if (current === lastUserInput) { // If nothing changed (e.g., selection change only), still try to render renderLatex(lastFixed || current); return; } const fixed = autoFixLatex(current); lastUserInput = current; lastFixed = fixed; // Only render if changed to reduce churn if (fixed !== lastRendered) { lastRendered = fixed; renderLatex(fixed); } else { renderLatex(fixed); } } editor.addEventListener('input', triggerUpdate); renderBtn.addEventListener('click', triggerUpdate); copyTexBtn.addEventListener('click', async () => { try { const text = lastFixed || editor.value || ''; await navigator.clipboard.writeText(text); copyTexBtn.textContent = 'Copied!'; setTimeout(() => (copyTexBtn.textContent = 'Copy LaTeX'), 1200); } catch { alert('Copy failed. Please select and copy manually.'); } }); clearBtn.addEventListener('click', () => { editor.value = ''; lastUserInput = ''; lastFixed = ''; lastRendered = ''; renderLatex(''); editor.focus(); }); // Initial render with sample editor.value = String.raw`\int_{0}^{\infty} e^{-x^2} \, dx = \frac{\sqrt{\pi}}{2}`; triggerUpdate();