57 lines
6.1 KiB
JavaScript
57 lines
6.1 KiB
JavaScript
/*
|
|
* 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 { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown";
|
|
import { toast } from "@rustpad/toast";
|
|
|
|
const token = location.pathname.split("/").filter(Boolean)[1];
|
|
const content = document.querySelector("#public-content");
|
|
const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
|
|
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 = "";
|
|
function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; }
|
|
async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
|
async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } 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 scrollToPublicAnchor(hash, behavior = "auto") {
|
|
const rawId = String(hash || "").replace(/^#/, "");
|
|
if (!rawId) return false;
|
|
let id;
|
|
try { id = decodeURIComponent(rawId); } catch { id = rawId; }
|
|
const target = document.getElementById(id);
|
|
if (!target || !content.contains(target)) return false;
|
|
target.scrollIntoView({ behavior, block: "start" });
|
|
return true;
|
|
}
|
|
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); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { 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", event => {
|
|
const link = event.target.closest('.markdown-toc a[href^="#"]');
|
|
if (!link) return;
|
|
const hash = link.getAttribute("href");
|
|
if (!scrollToPublicAnchor(hash, "smooth")) return;
|
|
event.preventDefault();
|
|
history.replaceState(null, "", `${location.pathname}${location.search}${hash}`);
|
|
});
|
|
window.addEventListener("hashchange", () => 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); });
|
|
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();
|