Mercurial
view mrjunejune/src/tools/latex_editor/index.js @ 246:4f2b50bc78e7
[tools] Fix LaTeX editor visibility
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 03 Aug 2026 18:30:45 -0700 |
| parents | 3843bb6253ac |
| children |
line wrap: on
line source
(function latexEditor() { "use strict"; const SOURCE_LIMIT = 64 * 1024; const STORAGE_KEY = "mrjunejune-latex-source"; const AUTO_COMPILE_DELAY = 500; window.addEventListener("DOMContentLoaded", () => { const source = document.querySelector("#latexSource"); const compileButton = document.querySelector("#compileButton"); const resetButton = document.querySelector("#resetButton"); const autoCompile = document.querySelector("#autoCompile"); const status = document.querySelector("#latexStatus"); const sourceSize = document.querySelector("#sourceSize"); const preview = document.querySelector("#pdfPreview"); const diagnostics = document.querySelector("#latexDiagnostics"); const download = document.querySelector("#downloadButton"); if (!source || !compileButton || !resetButton || !autoCompile || !status || !sourceSize || !preview || !diagnostics || !download) return; const defaultSource = source.value; const savedSource = localStorage.getItem(STORAGE_KEY); if (savedSource) source.value = savedSource; let currentPdfUrl = null; let compileGeneration = 0; let controller = null; let compilePending = false; let retryDelay = 0; const byteLength = value => new TextEncoder().encode(value).byteLength; const setStatus = (message, state) => { status.textContent = message; status.dataset.state = state; }; const updateSize = () => { const size = byteLength(source.value); sourceSize.textContent = `${size.toLocaleString()} / ${SOURCE_LIMIT.toLocaleString()} bytes`; sourceSize.dataset.overLimit = size > SOURCE_LIMIT ? "true" : "false"; return size; }; const clearPdf = () => { if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl); currentPdfUrl = null; preview.src = "about:blank"; download.removeAttribute("href"); download.removeAttribute("download"); download.classList.add("disabled"); download.setAttribute("aria-disabled", "true"); }; const showDiagnostics = message => { clearPdf(); diagnostics.textContent = message; diagnostics.hidden = false; preview.hidden = true; }; const showPdf = blob => { if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl); currentPdfUrl = URL.createObjectURL(blob); diagnostics.hidden = true; preview.hidden = false; preview.src = currentPdfUrl; download.href = currentPdfUrl; download.download = "document.pdf"; download.classList.remove("disabled"); download.setAttribute("aria-disabled", "false"); }; const createDebouncer = (callback, delay) => { let timer = null; const schedule = (nextDelay = delay) => { clearTimeout(timer); timer = setTimeout(() => { timer = null; callback(); }, nextDelay); }; schedule.cancel = () => { clearTimeout(timer); timer = null; }; return schedule; }; let queueCompile; const compile = async () => { queueCompile.cancel(); const generation = ++compileGeneration; const text = source.value; const size = updateSize(); if (!text.trim()) { showDiagnostics("Write some LaTeX before compiling."); setStatus("Nothing to compile.", "error"); return; } if (size > SOURCE_LIMIT) { showDiagnostics("The server accepts at most 64 KiB of LaTeX source."); setStatus("Source is too large.", "error"); return; } if (controller) { compilePending = true; setStatus("Finishing the current PDF, then compiling your latest changes...", "working"); return; } compilePending = false; const requestController = new AbortController(); controller = requestController; compileButton.disabled = true; setStatus("Compiling on the server...", "working"); try { const response = await fetch("/api/latex/render", { method: "POST", headers: { "Content-Type": "text/plain; charset=utf-8", }, body: text, cache: "no-store", signal: requestController.signal, }); if (generation !== compileGeneration) return; if (!response.ok) { if (response.status === 429) { compilePending = true; retryDelay = 1000; setStatus("The compiler is busy. Your latest changes are queued.", "working"); return; } const message = await response.text(); showDiagnostics(message || `Compilation failed (${response.status}).`); setStatus("Fix the LaTeX errors shown in the preview pane.", "error"); return; } const blob = await response.blob(); if (blob.type !== "application/pdf" || blob.size < 5) { throw new Error("The server returned an invalid PDF."); } showPdf(blob); setStatus( `PDF ready (${Math.ceil(blob.size / 1024).toLocaleString()} KiB).`, "ready", ); } catch (error) { if (error.name === "AbortError" || generation !== compileGeneration) return; showDiagnostics(error.message || "Unable to compile this document."); setStatus("The PDF request failed.", "error"); } finally { if (controller === requestController) { controller = null; compileButton.disabled = false; } if (compilePending && !controller) { compilePending = false; const delay = retryDelay; retryDelay = 0; queueCompile(delay); } } }; queueCompile = createDebouncer(compile, AUTO_COMPILE_DELAY); const invalidateCompile = () => { compileGeneration++; compilePending = false; retryDelay = 0; queueCompile.cancel(); }; source.addEventListener("input", () => { localStorage.setItem(STORAGE_KEY, source.value); invalidateCompile(); const size = updateSize(); if (!source.value.trim()) { showDiagnostics("Write some LaTeX before compiling."); setStatus("Nothing to compile.", "error"); return; } if (size > SOURCE_LIMIT) { showDiagnostics("The server accepts at most 64 KiB of LaTeX source."); setStatus("Source is too large.", "error"); return; } setStatus( autoCompile.checked ? "Waiting to compile..." : "Changes ready to compile.", "working", ); if (autoCompile.checked) queueCompile(); }); source.addEventListener("keydown", event => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); compile(); } }); compileButton.addEventListener("click", compile); resetButton.addEventListener("click", () => { source.value = defaultSource; localStorage.removeItem(STORAGE_KEY); updateSize(); compile(); }); autoCompile.addEventListener("change", () => { if (autoCompile.checked) queueCompile(); else { queueCompile.cancel(); compilePending = false; } }); window.addEventListener("beforeunload", () => { if (controller) controller.abort(); if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl); }); updateSize(); compile(); }); })();