Spaces:
Sleeping
Sleeping
| /* 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 => `<div class="error-item">${escHtml(l)}</div>`) | |
| .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 => `<div class="error-item">β ${escHtml(e)}</div>`) | |
| .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, '<') | |
| .replace(/>/g, '>'); | |
| } | |