new functions and fixes
This commit is contained in:
+28
-12
@@ -2101,6 +2101,15 @@ dialog::backdrop {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
}
|
||||
|
||||
.public-page.public-full-width .public-header {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.public-page.public-full-width .public-document {
|
||||
width: calc(100% - 32px);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.public-content {
|
||||
padding: clamp(24px, 4vw, 52px);
|
||||
}
|
||||
@@ -4860,27 +4869,34 @@ dialog::backdrop {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc ol {
|
||||
.markdown-body .markdown-toc ul {
|
||||
margin: 0;
|
||||
padding-left: 1.4em;
|
||||
padding-left: 1.35em;
|
||||
}
|
||||
|
||||
/* Top-level entries are already numbered by their headings, so omit redundant bullets. */
|
||||
.markdown-body .markdown-toc > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc > ul > li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc ul ul {
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc li {
|
||||
margin: .25em 0;
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc .toc-level-2 {
|
||||
margin-left: 1em;
|
||||
.markdown-body .markdown-toc li::marker {
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc .toc-level-3 {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-body .markdown-toc .toc-level-4,
|
||||
.markdown-body .markdown-toc .toc-level-5,
|
||||
.markdown-body .markdown-toc .toc-level-6 {
|
||||
margin-left: 3em;
|
||||
.markdown-body .markdown-toc :is(.toc-level-2, .toc-level-3, .toc-level-4, .toc-level-5, .toc-level-6) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Nested Markdown lists keep markers and source-line numbers in separate gutters. */
|
||||
|
||||
+27
-5
@@ -152,7 +152,10 @@ function inline(value) {
|
||||
});
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return stash(`<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer"${titleAttr}>${label}</a>`);
|
||||
const navigationAttrs = String(url).trim().startsWith("#")
|
||||
? ""
|
||||
: ' target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer"';
|
||||
return stash(`<a href="${safeUrl(url)}"${navigationAttrs}${titleAttr}>${label}</a>`);
|
||||
});
|
||||
html = html.replace(/\[\^([^\]\s]+)\]/g, (_, id) => stash(`<sup class="footnote-ref"><a href="#fn-${escapeHtml(id)}" id="fnref-${escapeHtml(id)}">?</a></sup>`));
|
||||
|
||||
@@ -335,6 +338,26 @@ function collectHeadings(lines) {
|
||||
return headings;
|
||||
}
|
||||
|
||||
function tableOfContentsTree(headings) {
|
||||
const roots = [];
|
||||
const stack = [];
|
||||
|
||||
headings.forEach(item => {
|
||||
while (stack.length && item.level <= stack[stack.length - 1].level) stack.pop();
|
||||
const node = { ...item, children: [] };
|
||||
const parent = stack[stack.length - 1];
|
||||
(parent ? parent.children : roots).push(node);
|
||||
stack.push(node);
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
function renderTableOfContentsList(nodes) {
|
||||
if (!nodes.length) return "";
|
||||
return `<ul>${nodes.map(node => `<li class="toc-level-${node.level}"><a href="#${escapeHtml(node.id)}">${inline(node.text)}</a>${renderTableOfContentsList(node.children)}</li>`).join("")}</ul>`;
|
||||
}
|
||||
|
||||
|
||||
export function alignPreviewLineNumbers(root) {
|
||||
if (!root) return;
|
||||
@@ -454,10 +477,9 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
|
||||
if (/^\s*\[TOC\]\s*$/i.test(line)) {
|
||||
closeList();
|
||||
if (headings.length) {
|
||||
html += `<nav class="markdown-toc preview-source-line" data-source-line="${index + lineOffset + 1}" aria-label="Table of contents"><ol>`;
|
||||
headings.forEach(item => html += `<li class="toc-level-${item.level}"><a href="#${escapeHtml(item.id)}">${inline(item.text)}</a></li>`);
|
||||
html += `</ol></nav>`;
|
||||
const tocHeadings = headings.filter(item => item.index > index);
|
||||
if (tocHeadings.length) {
|
||||
html += `<nav class="markdown-toc preview-source-line" data-source-line="${index + lineOffset + 1}" aria-label="Table of contents">${renderTableOfContentsList(tableOfContentsTree(tocHeadings))}</nav>`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
+22
-3
@@ -20,6 +20,8 @@ import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-lib
|
||||
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;
|
||||
@@ -57,21 +59,38 @@ function scrollToPublicAnchor(hash, behavior = "auto") {
|
||||
try { id = decodeURIComponent(rawId); } catch { id = rawId; }
|
||||
const target = document.getElementById(id);
|
||||
if (!target || !content.contains(target)) return false;
|
||||
for (let details = target.closest("details"); details; details = details.parentElement?.closest("details")) details.open = true;
|
||||
target.scrollIntoView({ behavior, block: "start" });
|
||||
return true;
|
||||
}
|
||||
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); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); 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 link = event.target.closest('a[href^="#"]');
|
||||
if (!link || !content.contains(link)) return;
|
||||
const hash = link.getAttribute("href");
|
||||
if (!scrollToPublicAnchor(hash, "smooth")) return;
|
||||
event.preventDefault();
|
||||
if (!hash || hash === "#" || !scrollToPublicAnchor(hash, "smooth")) return;
|
||||
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); });
|
||||
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); } });
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<a class="brand home-brand" href="/">RustPad</a>
|
||||
<div class="public-header__actions">
|
||||
<label class="line-toggle"><input id="public-line-numbers-toggle" type="checkbox"> Line numbers</label>
|
||||
<label class="line-toggle"><input id="public-full-width-toggle" type="checkbox"> Full width</label>
|
||||
<button id="copy-public-link" class="secondary-button">Copy link</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user