/* * Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ import { installGlobalDiagnostics, logInfo } from "@rustpad/logger"; installGlobalDiagnostics(); import { api } from "@rustpad/api"; import { copyText } from "@rustpad/clipboard"; import { lineFromHash, lineLink } from "@rustpad/line-links"; import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown"; import { toast } from "@rustpad/toast"; import { getTheme } from "@rustpad/theme"; import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs"; const token = location.pathname.split("/").filter(Boolean)[1]; const content = document.querySelector("#public-content"); const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle"); const fullWidthToggle = document.querySelector("#public-full-width-toggle"); const fullWidthStorageKey = "rustpad:public-full-width"; const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error"); let pagePassword = ""; let mermaidRenderVersion = 0; let publicAnchorSettleCleanup = null; function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; } async function renderMermaid() { const renderVersion = ++mermaidRenderVersion; const nodes = [...content.querySelectorAll(".mermaid")]; if (!nodes.length) return; try { const mermaid = await loadMermaid(); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes }); } catch { if (renderVersion !== mermaidRenderVersion) return; nodes.forEach(node => { if (!node.isConnected || !content.contains(node) || !node.parentNode) return; const message = document.createElement("p"); message.className = "error mermaid-error"; message.textContent = "Failed to load Mermaid."; node.parentNode.insertBefore(message, node); }); } } async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await loadHighlight(); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } catch { } } async function renderMediaPlayers() { const nodes = content.querySelectorAll("[data-rustpad-player]"); if (!nodes.length) return; try { const { hydrateMediaPlayers } = await loadMediaPlayer(); hydrateMediaPlayers(content); } catch { } } function lockPublicContent(allowTaskUpdates) { content.querySelectorAll('[contenteditable]').forEach(node => node.removeAttribute('contenteditable')); content.querySelectorAll('.preview-editable').forEach(node => node.classList.remove('preview-editable')); content.querySelectorAll('.task-checkbox').forEach(box => { box.disabled = !allowTaskUpdates; box.title = allowTaskUpdates ? 'Update this task' : 'Task updates are disabled by the owner'; }); } function publicAnchorTarget(hash) { const rawId = String(hash || "").replace(/^#/, ""); if (!rawId) return null; let id; try { id = decodeURIComponent(rawId); } catch { id = rawId; } const target = document.getElementById(id); if (target && content.contains(target)) return target; const line = lineFromHash(hash); return line ? content.querySelector(`.preview-source-line[data-source-line="${line}"]`) : null; } function markPublicAnchor(target) { content.querySelectorAll(".is-public-link-target").forEach(node => node.classList.remove("is-public-link-target")); target?.classList.add("is-public-link-target"); } function scrollToPublicAnchor(hash, behavior = "auto") { const target = publicAnchorTarget(hash); if (!target) return false; for (let details = target.closest("details"); details; details = details.parentElement?.closest("details")) details.open = true; markPublicAnchor(target); target.scrollIntoView({ behavior, block: "start" }); return true; } function settleInitialPublicAnchor(hash) { publicAnchorSettleCleanup?.(); publicAnchorSettleCleanup = null; if (!hash) return; let stopped = false; let frame = 0; let observer = null; const cleanups = []; const cleanup = () => { if (stopped) return; stopped = true; cancelAnimationFrame(frame); observer?.disconnect(); cleanups.splice(0).forEach(remove => remove()); if (publicAnchorSettleCleanup === cleanup) publicAnchorSettleCleanup = null; }; const correct = () => { frame = 0; if (stopped || location.hash !== hash) return; scrollToPublicAnchor(hash); }; const scheduleCorrection = () => { if (stopped || frame) return; frame = requestAnimationFrame(correct); }; const stopOnUserInput = () => cleanup(); ["wheel", "touchstart", "pointerdown", "keydown"].forEach(type => { window.addEventListener(type, stopOnUserInput, { capture: true, passive: true }); cleanups.push(() => window.removeEventListener(type, stopOnUserInput, true)); }); content.querySelectorAll("img, video, iframe").forEach(asset => { ["load", "error", "loadedmetadata"].forEach(type => { asset.addEventListener(type, scheduleCorrection, { once: true }); cleanups.push(() => asset.removeEventListener(type, scheduleCorrection)); }); }); if (document.readyState !== "complete") { window.addEventListener("load", scheduleCorrection, { once: true }); cleanups.push(() => window.removeEventListener("load", scheduleCorrection)); } document.fonts?.ready?.then(scheduleCorrection).catch(() => {}); if (typeof ResizeObserver === "function") { observer = new ResizeObserver(scheduleCorrection); observer.observe(content); } publicAnchorSettleCleanup = cleanup; scheduleCorrection(); requestAnimationFrame(() => requestAnimationFrame(scheduleCorrection)); const timeout = window.setTimeout(cleanup, 6000); cleanups.push(() => clearTimeout(timeout)); } function isBlankPublicSourceLine(target) { if (!target.matches("div.preview-source-line")) return false; return ![...target.childNodes].some(node => { if (node.nodeType === Node.TEXT_NODE) return Boolean(node.textContent?.trim()); if (!(node instanceof Element)) return false; return !node.matches("br, .public-permalink"); }); } function installPublicPermalinks() { content.querySelectorAll(".preview-source-line[data-source-line]").forEach(target => { if (target.matches("hr") || isBlankPublicSourceLine(target) || target.querySelector(":scope > .public-permalink")) return; const line = Number(target.dataset.sourceLine); if (!Number.isSafeInteger(line) || line < 1) return; const heading = target.matches("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]"); const anchor = document.createElement("a"); anchor.className = "public-permalink"; anchor.href = heading ? `#${encodeURIComponent(target.id)}` : `#L${line}`; anchor.dataset.line = String(line); anchor.dataset.linkKind = heading ? "heading" : "line"; anchor.textContent = String(line); anchor.setAttribute("aria-label", heading ? `Copy link to heading on line ${line}` : `Copy link to line ${line}`); anchor.title = heading ? `Copy link to this heading (line ${line})` : `Copy link to line ${line}`; target.append(anchor); }); } function setFullWidth(enabled, { persist = true } = {}) { const active = Boolean(enabled); document.body.classList.toggle("public-full-width", active); fullWidthToggle.checked = active; if (persist) { try { localStorage.setItem(fullWidthStorageKey, active ? "1" : "0"); } catch { } } requestAnimationFrame(() => alignPreviewLineNumbers(content)); } function restoreFullWidth() { let enabled = false; try { enabled = localStorage.getItem(fullWidthStorageKey) === "1"; } catch { } setFullWidth(enabled, { persist: false }); } async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); installPublicPermalinks(); alignPreviewLineNumbers(content); settleInitialPublicAnchor(location.hash); } catch (error) { publicAnchorSettleCleanup?.(); if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } } content.addEventListener("click", async event => { const link = event.target.closest('a[href^="#"]'); if (!link || !content.contains(link)) return; const hash = link.getAttribute("href"); event.preventDefault(); if (!hash || hash === "#" || !scrollToPublicAnchor(hash, "smooth")) return; history.replaceState(null, "", `${location.pathname}${location.search}${hash}`); if (!link.classList.contains("public-permalink")) return; try { const href = link.dataset.linkKind === "heading" ? new URL(hash, location.href).href : lineLink(location.href, Number(link.dataset.line)); await copyText(href); link.classList.add("is-copied"); setTimeout(() => link.classList.remove("is-copied"), 900); toast(link.dataset.linkKind === "heading" ? "Heading link copied" : `Link to line ${link.dataset.line} copied`); } catch (error) { toast(error.message); } }); window.addEventListener("hashchange", () => { publicAnchorSettleCleanup?.(); scrollToPublicAnchor(location.hash, "smooth"); }); content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } }); lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); }); fullWidthToggle.addEventListener("change", () => setFullWidth(fullWidthToggle.checked)); window.addEventListener("resize", () => requestAnimationFrame(() => alignPreviewLineNumbers(content))); passwordForm.addEventListener("submit", async event => { event.preventDefault(); pagePassword = passwordInput.value; await initialize(); }); passwordDialog.addEventListener("cancel", event => event.preventDefault()); document.querySelector("#copy-public-link").addEventListener("click", async () => { try { await copyText(location.href); toast("Link copied"); } catch (error) { toast(error.message); } }); initialize();