/* Editor Page — LaTeX editor with PDF.js preview (no iframe, sandbox-safe) */ const DEFAULT_LATEX = `\\documentclass[12pt,a4paper]{article} \\usepackage[margin=1in]{geometry} \\usepackage{amsmath} \\usepackage{graphicx} \\usepackage{hyperref} \\title{My Document} \\author{Your Name} \\date{\\today} \\begin{document} \\maketitle \\begin{abstract} Write your abstract here. This is a brief summary of your work. \\end{abstract} \\section{Introduction} Start writing your introduction here. You can use \\LaTeX{} commands to format your text. \\section{Main Content} This is the main content of your document. You can add: \\begin{itemize} \\item Bullet points \\item Mathematics: $E = mc^2$ \\item References and citations \\end{itemize} \\subsection{A Subsection} Add subsections as needed. \\begin{equation} \\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2} \\end{equation} \\section{Conclusion} Your conclusion goes here. \\end{document}`; const SNIPPETS = { equation: `\\begin{equation}\n f(x) = \\sum_{i=0}^{n} a_i x^i\n\\end{equation}`, table: `\\begin{table}[h]\n\\centering\n\\begin{tabular}{|l|c|r|}\n\\hline\nHeader 1 & Header 2 & Header 3 \\\\\n\\hline\nRow 1 & Value & Data \\\\\nRow 2 & Value & Data \\\\\n\\hline\n\\end{tabular}\n\\caption{Table caption}\n\\label{tab:mytable}\n\\end{table}`, figure: `\\begin{figure}[h]\n\\centering\n\\includegraphics[width=0.8\\linewidth]{filename.png}\n\\caption{Figure caption}\n\\label{fig:myfig}\n\\end{figure}`, itemize: `\\begin{itemize}\n \\item First item\n \\item Second item\n \\item Third item\n\\end{itemize}`, enumerate: `\\begin{enumerate}\n \\item First item\n \\item Second item\n \\item Third item\n\\end{enumerate}`, section: `\\section{Section Title}\n\nSection content here.\n\n\\subsection{Subsection}\n\nSubsection content.`, code: `\\usepackage{listings} % add to preamble\n\n\\begin{lstlisting}[language=Python]\ndef hello_world():\n print("Hello, World!")\n\\end{lstlisting}`, columns: `\\begin{columns}\n\\begin{column}{0.5\\textwidth}\n Left column content\n\\end{column}\n\\begin{column}{0.5\\textwidth}\n Right column content\n\\end{column}\n\\end{columns}`, }; // ─── State ──────────────────────────────────────────────────────────────────── let currentPdfBlob = null; // kept for PDF download let zoomLevel = 1.5; // PDF.js render scale let templateId = null; let pdfDoc = null; // loaded PDF.js document // ─── DOM refs ───────────────────────────────────────────────────────────────── const codeEditor = document.getElementById('code-editor'); const lineNumbers = document.getElementById('line-numbers'); const statusDot = document.querySelector('.status-dot'); const statusText = document.getElementById('status-text'); const canvasContainer = document.getElementById('pdf-canvas-container'); const previewPlaceholder = document.getElementById('preview-placeholder'); const previewLoading = document.getElementById('preview-loading'); const errorPanel = document.getElementById('error-panel'); const errorList = document.getElementById('error-list'); const docTitle = document.getElementById('doc-title'); // ─── Init ───────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { const params = new URLSearchParams(location.search); templateId = params.get('template'); if (templateId) { await loadTemplate(templateId); } else { codeEditor.value = DEFAULT_LATEX; updateLineNumbers(); } codeEditor.addEventListener('input', () => { updateLineNumbers(); setStatus('modified', 'Modified'); }); codeEditor.addEventListener('scroll', syncScroll); codeEditor.addEventListener('keydown', handleKeydown); document.getElementById('btn-compile').addEventListener('click', compileDocument); document.getElementById('placeholder-compile')?.addEventListener('click', compileDocument); document.getElementById('btn-check').addEventListener('click', checkSyntax); document.getElementById('btn-pdf').addEventListener('click', () => downloadFile('pdf')); document.getElementById('btn-docx').addEventListener('click', () => downloadFile('docx')); document.getElementById('btn-zoom-in').addEventListener('click', () => adjustZoom(0.25)); document.getElementById('btn-zoom-out').addEventListener('click', () => adjustZoom(-0.25)); document.getElementById('btn-fullscreen').addEventListener('click', toggleFullscreen); document.getElementById('btn-snippet').addEventListener('click', toggleSnippetPanel); document.getElementById('snippet-close').addEventListener('click', () => { document.getElementById('snippet-panel').style.display = 'none'; }); document.getElementById('error-close').addEventListener('click', () => { errorPanel.style.display = 'none'; }); document.getElementById('btn-upload-tex')?.addEventListener('click', () => { document.getElementById('tex-upload-input').click(); }); document.getElementById('tex-upload-input')?.addEventListener('change', handleTexUpload); document.querySelectorAll('.snippet-btn').forEach(btn => { btn.addEventListener('click', () => { const key = btn.dataset.snippet; if (SNIPPETS[key]) insertSnippet(SNIPPETS[key]); document.getElementById('snippet-panel').style.display = 'none'; }); }); initResizer(); document.getElementById('btn-format')?.addEventListener('click', formatCode); // Disable downloads until first compile document.getElementById('btn-pdf').disabled = true; document.getElementById('btn-docx').disabled = true; // Sync zoom label with initial render scale document.getElementById('zoom-label').textContent = `${Math.round(zoomLevel * 100)}%`; updateLineNumbers(); }); // ─── Template loader ────────────────────────────────────────────────────────── async function loadTemplate(id) { try { setStatus('compiling', 'Loading…'); const tpl = await api.getTemplate(id); codeEditor.value = tpl.latex_content || DEFAULT_LATEX; docTitle.value = tpl.name || 'Untitled'; updateLineNumbers(); setStatus('', 'Ready'); } catch (err) { codeEditor.value = DEFAULT_LATEX; updateLineNumbers(); setStatus('', 'Ready'); showToast(`Could not load template: ${err.message}`, 'error'); } } // ─── .tex file upload ───────────────────────────────────────────────────────── async function handleTexUpload(e) { const file = e.target.files[0]; if (!file) return; if (!file.name.endsWith('.tex')) { showToast('Please select a .tex file', 'error'); return; } const text = await file.text(); codeEditor.value = text; docTitle.value = file.name.replace('.tex', ''); updateLineNumbers(); setStatus('modified', 'Loaded from file'); showToast(`Loaded ${file.name}`, 'success'); e.target.value = ''; } // ─── Line Numbers ───────────────────────────────────────────────────────────── function updateLineNumbers() { const lines = codeEditor.value.split('\n').length; lineNumbers.textContent = Array.from({ length: lines }, (_, i) => i + 1).join('\n'); } function syncScroll() { lineNumbers.scrollTop = codeEditor.scrollTop; } // ─── Status ─────────────────────────────────────────────────────────────────── function setStatus(state, text) { statusDot.className = 'status-dot' + (state ? ` ${state}` : ''); statusText.textContent = text; } // ─── Keyboard Shortcuts ─────────────────────────────────────────────────────── function handleKeydown(e) { if (e.key === 'Tab') { e.preventDefault(); const start = codeEditor.selectionStart; const end = codeEditor.selectionEnd; codeEditor.value = codeEditor.value.substring(0, start) + ' ' + codeEditor.value.substring(end); codeEditor.selectionStart = codeEditor.selectionEnd = start + 2; updateLineNumbers(); } if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 's')) { e.preventDefault(); compileDocument(); } } // ─── Compile + PDF.js Render ────────────────────────────────────────────────── async function compileDocument() { const latex = codeEditor.value.trim(); if (!latex) { showToast('Editor is empty', 'error'); return; } setStatus('compiling', 'Compiling…'); previewPlaceholder.style.display = 'none'; previewLoading.style.display = 'flex'; canvasContainer.style.display = 'none'; const compileBtn = document.getElementById('btn-compile'); compileBtn.disabled = true; try { // Fetch compiled PDF as a blob (binary download from server) const blob = await api.compileDocument(latex, 'pdf'); currentPdfBlob = blob; // Render with PDF.js — works in any sandbox, no iframe at all await renderPdfBlob(blob); previewLoading.style.display = 'none'; canvasContainer.style.display = 'block'; errorPanel.style.display = 'none'; setStatus('success', 'Compiled'); showToast('Compiled successfully!', 'success'); document.getElementById('btn-pdf').disabled = false; document.getElementById('btn-docx').disabled = false; } catch (err) { previewLoading.style.display = 'none'; previewPlaceholder.style.display = 'flex'; setStatus('error', 'Error'); const errorMsg = err.message || 'Compilation failed'; errorList.innerHTML = errorMsg.split('\n') .filter(l => l.trim()) .map(l => `
${escHtml(l)}
`) .join(''); errorPanel.style.display = 'block'; showToast('Compilation failed', 'error'); } finally { compileBtn.disabled = false; } } // ─── PDF.js renderer ────────────────────────────────────────────────────────── async function renderPdfBlob(blob) { const arrayBuffer = await blob.arrayBuffer(); // Load the PDF document const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); pdfDoc = await loadingTask.promise; canvasContainer.innerHTML = ''; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: zoomLevel }); const wrapper = document.createElement('div'); wrapper.className = 'pdf-page-wrapper'; const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; wrapper.appendChild(canvas); canvasContainer.appendChild(wrapper); await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; } } async function rerenderPdf() { if (!pdfDoc) return; canvasContainer.innerHTML = ''; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: zoomLevel }); const wrapper = document.createElement('div'); wrapper.className = 'pdf-page-wrapper'; const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; wrapper.appendChild(canvas); canvasContainer.appendChild(wrapper); await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; } } // ─── Check Syntax ───────────────────────────────────────────────────────────── async function checkSyntax() { const latex = codeEditor.value.trim(); if (!latex) return; try { const result = await api.checkLatex(latex); if (result.valid) { errorPanel.style.display = 'none'; showToast('✓ Syntax looks good', 'success'); setStatus('success', 'Valid'); } else { errorList.innerHTML = result.errors .map(e => `
⚠ ${escHtml(e)}
`) .join(''); errorPanel.style.display = 'block'; setStatus('error', `${result.errors.length} issue(s)`); } } catch (err) { showToast(err.message, 'error'); } } // ─── Download ───────────────────────────────────────────────────────────────── async function downloadFile(format) { const latex = codeEditor.value.trim(); if (!latex) { showToast('Nothing to export', 'error'); return; } setStatus('compiling', `Exporting ${format.toUpperCase()}…`); try { let blob; if (format === 'pdf' && currentPdfBlob) { // Reuse the already-compiled PDF blob — no extra server call needed blob = currentPdfBlob; } else { blob = await api.compileDocument(latex, format); } const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const name = (docTitle.value || 'document').replace(/[^a-z0-9_-]/gi, '_'); a.download = `${name}.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setStatus('success', 'Downloaded'); showToast(`${format.toUpperCase()} downloaded!`, 'success'); } catch (err) { setStatus('error', 'Error'); showToast(`Export failed: ${err.message}`, 'error'); } } // ─── Zoom (re-renders at new scale) ────────────────────────────────────────── async function adjustZoom(delta) { zoomLevel = Math.max(0.5, Math.min(3.0, zoomLevel + delta)); document.getElementById('zoom-label').textContent = `${Math.round(zoomLevel * 100)}%`; if (pdfDoc) await rerenderPdf(); } // ─── Fullscreen ─────────────────────────────────────────────────────────────── function toggleFullscreen() { const codePanel = document.getElementById('code-panel'); const resizer = document.getElementById('resizer'); const previewPanel = document.getElementById('preview-panel'); if (codePanel.style.display === 'none') { codePanel.style.display = 'flex'; resizer.style.display = ''; previewPanel.style.flex = ''; } else { codePanel.style.display = 'none'; resizer.style.display = 'none'; previewPanel.style.flex = '1'; } } // ─── Snippet ────────────────────────────────────────────────────────────────── function insertSnippet(text) { const start = codeEditor.selectionStart; codeEditor.value = codeEditor.value.substring(0, start) + '\n' + text + '\n' + codeEditor.value.substring(start); codeEditor.selectionStart = codeEditor.selectionEnd = start + text.length + 2; codeEditor.focus(); updateLineNumbers(); } function toggleSnippetPanel() { const panel = document.getElementById('snippet-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; } // ─── Format ─────────────────────────────────────────────────────────────────── function formatCode() { let code = codeEditor.value; code = code.replace(/([^\n])\n(\\(?:section|subsection|subsubsection|begin|end)\{)/g, '$1\n\n$2'); codeEditor.value = code; updateLineNumbers(); showToast('Formatted', 'success'); } // ─── Resizer ────────────────────────────────────────────────────────────────── function initResizer() { const resizer = document.getElementById('resizer'); const codePanel = document.getElementById('code-panel'); const layout = document.querySelector('.editor-layout'); let startX, startWidth; resizer.addEventListener('mousedown', e => { startX = e.clientX; startWidth = codePanel.offsetWidth; resizer.classList.add('dragging'); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); }); function onMouseMove(e) { const newWidth = Math.max(200, Math.min(layout.offsetWidth - 200, startWidth + e.clientX - startX)); codePanel.style.width = newWidth + 'px'; codePanel.style.flex = 'none'; } function onMouseUp() { resizer.classList.remove('dragging'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } } // ─── Utils ──────────────────────────────────────────────────────────────────── function escHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>'); }