const DEFAULT_LATEX = `\\documentclass{article} \\usepackage[utf8]{inputenc} \\usepackage{amsmath} \\usepackage{graphicx} \\title{My Document} \\author{Author Name} \\date{\\today} \\begin{document} \\maketitle \\section{Introduction} Welcome to the \\LaTeX\\ editor! Start typing to see instant preview. \\section{Mathematics} Here's a famous equation: \\[ E = mc^2 \\] And an inline equation: $a^2 + b^2 = c^2$ \\section{Lists} \\begin{itemize} \\item First item \\item Second item \\item Third item \\end{itemize} \\end{document}`; const editor = CodeMirror.fromTextArea(document.getElementById('editor'), { mode: 'stex', theme: 'dracula', lineNumbers: true, lineWrapping: true, autofocus: true, tabSize: 4, indentUnit: 4, matchBrackets: true, autoCloseBrackets: true }); editor.setValue(DEFAULT_LATEX); const statusEl = document.getElementById('status'); const pdfCanvas = document.getElementById('pdf-canvas'); const pdfContainer = document.getElementById('pdf-container'); const placeholder = document.getElementById('placeholder'); const ctx = pdfCanvas.getContext('2d'); let currentPdf = null; let currentPage = 1; let scale = 1.5; let debounceTimer = null; let ws = null; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setStatus(text, type = 'info') { statusEl.textContent = text; statusEl.className = 'status ' + type; } function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${protocol}//${window.location.host}/ws`); ws.onopen = () => { setStatus('Connected', 'success'); reconnectAttempts = 0; compile(); }; ws.onmessage = async (event) => { const data = JSON.parse(event.data); if (data.type === 'compiling') { setStatus('Compiling...', 'info'); } else if (data.type === 'pdf') { setStatus('Ready', 'success'); await renderPdf(data.data); } else if (data.type === 'error') { setStatus('Compilation Error', 'error'); showError(data.log); } }; ws.onclose = () => { setStatus('Disconnected', 'error'); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(connectWebSocket, 1000 * reconnectAttempts); } }; ws.onerror = () => { setStatus('Connection Error', 'error'); }; } function compile() { if (ws && ws.readyState === WebSocket.OPEN) { const latex = editor.getValue(); ws.send(JSON.stringify({ type: 'compile', latex })); } } function debounceCompile() { clearTimeout(debounceTimer); debounceTimer = setTimeout(compile, 500); } async function renderPdf(base64Data) { try { const pdfData = atob(base64Data); const uint8Array = new Uint8Array(pdfData.length); for (let i = 0; i < pdfData.length; i++) { uint8Array[i] = pdfData.charCodeAt(i); } currentPdf = await pdfjsLib.getDocument({ data: uint8Array }).promise; await renderPage(currentPage); placeholder.style.display = 'none'; pdfCanvas.style.display = 'block'; } catch (e) { console.error('PDF render error:', e); } } async function renderPage(pageNum) { if (!currentPdf) return; const page = await currentPdf.getPage(pageNum); const viewport = page.getViewport({ scale }); pdfCanvas.height = viewport.height; pdfCanvas.width = viewport.width; await page.render({ canvasContext: ctx, viewport: viewport }).promise; } function showError(log) { placeholder.innerHTML = `
${escapeHtml(log)}`;
placeholder.style.display = 'block';
pdfCanvas.style.display = 'none';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.getElementById('zoom-in').addEventListener('click', async () => {
scale = Math.min(scale + 0.25, 3);
document.getElementById('zoom-level').textContent = Math.round(scale / 1.5 * 100) + '%';
await renderPage(currentPage);
});
document.getElementById('zoom-out').addEventListener('click', async () => {
scale = Math.max(scale - 0.25, 0.5);
document.getElementById('zoom-level').textContent = Math.round(scale / 1.5 * 100) + '%';
await renderPage(currentPage);
});
const resizer = document.getElementById('resizer');
const editorPanel = document.querySelector('.editor-panel');
let isResizing = false;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const container = document.querySelector('.main');
const containerRect = container.getBoundingClientRect();
const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100;
const clamped = Math.min(Math.max(percentage, 20), 80);
editorPanel.style.width = clamped + '%';
});
document.addEventListener('mouseup', () => {
isResizing = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
editor.on('change', debounceCompile);
connectWebSocket();