/* Event listener function for the COPY BUTTON */ document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll("pre.src").forEach(function (codeBlock) { const button = document.createElement("button"); button.innerText = "Copy"; button.className = "copy-btn"; // Append button inside
codeBlock.appendChild(button);
button.addEventListener("click", function () {
const text = codeBlock.innerText.replace(button.innerText, ""); // exclude button text
navigator.clipboard.writeText(text.trim()).then(() => {
button.innerText = "Copied!";
setTimeout(() => (button.innerText = "Copy"), 1500);
});
});
});
});
/* Event listener for footnotes and sidenotes*/
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('a.footref[href^="#fn"]').forEach(ref => {
const sup = ref.closest("sup") || ref;
// idempotent: don't insert twice
if (sup.nextElementSibling && sup.nextElementSibling.classList?.contains("footnote-sidenote")) return;
const targetId = ref.getAttribute("href").replace(/^#/, ""); // works for fn.2 or fn2
const anchor = document.getElementById(targetId);
if (!anchor) return;
const footdef = anchor.closest(".footdef") || anchor.parentElement;
if (!footdef) return;
// 1) Prefer leaf paragraphs to avoid div+p duplication
let paras = footdef.querySelectorAll("p.footpara");
if (!paras.length) {
// fallback: any .footpara elements that don't contain another .footpara
paras = footdef.querySelectorAll(".footpara:not(:has(.footpara))");
}
// 2) Build HTML, de-duplicating by text content
let parts = [];
if (paras.length) {
const seen = new Set();
parts = Array.from(paras).map(p => {
const txt = p.textContent.trim().replace(/\s+/g, " ");
if (seen.has(txt)) return "";
seen.add(txt);
return p.innerHTML.trim();
}).filter(Boolean);
}
// 3) Fallback: clean full block if no paras found
if (!parts.length) {
const clone = footdef.cloneNode(true);
clone.querySelectorAll("sup.footnum, a[role='doc-backlink']").forEach(n => n.remove());
parts = [clone.innerHTML.trim()];
}
// 4) Insert the sidenote
const sn = document.createElement("span");
sn.className = "sidenote footnote-sidenote";
sn.setAttribute("data-fn", (ref.textContent || "").trim());
sn.innerHTML = parts.join(" ");
sup.insertAdjacentElement("afterend", sn);
});
});
/* Function for setting the theme */
(function(){
const root = document.documentElement;
const storageKey = "theme";
const themes = ["light", "dark", "dark-academia"];
const labels = {
"light": "Light",
"dark": "Dark",
"dark-academia": "Academia"
};
const saved = localStorage.getItem(storageKey);
if (themes.includes(saved)) {
root.setAttribute("data-theme", saved);
}
const btn = document.getElementById("theme-toggle");
if (!btn) return;
const updateButton = () => {
const current = root.getAttribute("data-theme");
const label = labels[current] || "Auto";
btn.textContent = `Theme: ${label}`;
btn.setAttribute("aria-label", `Current theme: ${label}. Switch theme.`);
};
updateButton();
btn.addEventListener("click", () => {
const current = root.getAttribute("data-theme");
const index = themes.indexOf(current);
const target = themes[index === -1 ? 1 : (index + 1) % themes.length];
root.setAttribute("data-theme", target);
localStorage.setItem(storageKey, target);
updateButton();
});
})();
// Simple, timezone-safe if you pass UTC (…Z) in the datetime
document.addEventListener('DOMContentLoaded', () => {
const els = document.querySelectorAll('time.countdown');
if (!els.length) return;
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
const render = (el) => {
const raw = el.getAttribute('datetime');
const label = el.dataset.label || '';
const target = new Date(raw); // Prefer ISO like 2025-12-31T00:00:00Z
if (isNaN(target)) { el.textContent = '—'; return; }
const now = new Date();
let diff = target - now;
if (diff <= 0) {
el.textContent = `${label ? label + ' ' : ''}today`;
el.classList.add('expired');
return;
}
const d = Math.floor(diff / 86400000); diff -= d * 86400000;
const h = Math.floor(diff / 3600000); diff -= h * 3600000;
const m = Math.floor(diff / 60000); diff -= m * 60000;
const s = Math.floor(diff / 1000);
const pieces = [];
if (d) pieces.push(plural(d, 'day'));
pieces.push(`${h}h ${m}m ${s}s`);
el.textContent = `${label ? label + ' in : ' : ''}${pieces.join(' ')}`;
};
const tick = () => els.forEach(render);
tick();
setInterval(tick, 1000); // update every second
});
/* Event listener for scrolling and changing the active label on the TOC */
document.addEventListener("DOMContentLoaded", () => {
const toc = document.querySelector("#text-table-of-contents");
if (!toc) { console.warn("No #text-table-of-contents found"); return; }
const links = toc.querySelectorAll('a[href^="#"]'); // '^=' is a starts with operator.
// Intro matches
if (!links.length) { console.warn("No ToC links found"); return; }
// Map: id -> link
const linkById = new Map();
links.forEach(a => {
const id = decodeURIComponent(a.getAttribute("href").slice(1));
const el = document.getElementById(id);
if (el) linkById.set(id, a);
});
if (!linkById.size) { console.warn("No matching headings with IDs"); return; }
// Headings to observe (h2–h4 usually)
const headings = [...document.querySelectorAll("h2[id], h3[id], h4[id]")]
.filter(h => linkById.has(h.id));
// Helper to mark active
const setActive = (id) => {
links.forEach(a => {
const active = a.getAttribute("href") === `#${id}`;
a.classList.toggle("is-active", active);
if (active) a.setAttribute("aria-current", "true");
else a.removeAttribute("aria-current");
});
};
// Calculate sticky header offset in px
const headerOffsetPx = 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize);
// Track visible headings (id -> distance from top)
const visible = new Map();
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting) {
// How far from the top (after header offset)
const dist = entry.target.getBoundingClientRect().top - headerOffsetPx;
visible.set(id, dist);
} else {
visible.delete(id);
}
});
if (visible.size) {
// Choose the heading closest to the top (>= -headerOffset)
const topMost = [...visible.entries()]
.sort((a,b) => Math.abs(a[1]) - Math.abs(b[1]))[0][0];
setActive(topMost);
// console.log("Active:", topMost, visible);
}
}, {
root: null, // track relative to viewport
rootMargin: `-${headerOffsetPx}px 0px -70% 0px`,
threshold: [0, 0.01, 0.1] // fire as soon as it enters
});
headings.forEach(h => observer.observe(h));
// Initial highlight (in case load mid‑page)
let bestId = null, bestDist = Infinity;
headings.forEach(h => {
const top = h.getBoundingClientRect().top - headerOffsetPx;
const dist = top < 0 ? Math.abs(top) : top + 1e6;
if (dist < bestDist) { bestDist = dist; bestId = h.id; }
});
if (bestId) setActive(bestId);
// smooth-scroll ToC clicks
toc.addEventListener("click", (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const id = decodeURIComponent(a.hash.slice(1));
const el = document.getElementById(id);
if (!el) return;
e.preventDefault();
el.scrollIntoView({ behavior: "smooth", block: "start" });
el.setAttribute("tabindex", "-1");
el.focus({ preventScroll: true });
history.pushState(null, "", `#${id}`);
});
});
// Make Mermaid diagrams use the active site theme and zoom controls.
(function () {
function cssVar(name, fallback) {
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
}
function mermaidConfig() {
return {
startOnLoad: false,
theme: "base",
themeVariables: {
background: cssVar("--bg", "#faf9f6"),
mainBkg: cssVar("--surface", "#ffffff"),
primaryColor: cssVar("--surface", "#ffffff"),
primaryTextColor: cssVar("--fg", "#000000"),
primaryBorderColor: cssVar("--accent", "#2563eb"),
secondaryColor: cssVar("--surface-soft", "#f3f1eb"),
tertiaryColor: cssVar("--bg", "#faf9f6"),
clusterBkg: cssVar("--surface-soft", "#f3f1eb"),
clusterBorder: cssVar("--border", "#d7d7d7"),
lineColor: cssVar("--border", "#d7d7d7"),
textColor: cssVar("--fg", "#000000"),
edgeLabelBackground: cssVar("--surface", "#ffffff"),
fontFamily: cssVar("--font-body", "Noto, system-ui, sans-serif")
},
flowchart: {
htmlLabels: true,
curve: "basis"
}
};
}
function decodeMermaidSource(source) {
const textarea = document.createElement("textarea");
textarea.innerHTML = source;
return textarea.value.trim();
}
async function renderMermaid() {
if (!window.mermaid) return;
const diagrams = document.querySelectorAll(".mermaid");
if (!diagrams.length) return;
diagrams.forEach((el) => {
if (!el.dataset.source) {
el.dataset.source = decodeMermaidSource(el.textContent || el.innerHTML);
}
el.removeAttribute("data-processed");
el.innerHTML = el.dataset.source;
});
mermaid.initialize(mermaidConfig());
await mermaid.run({ querySelector: ".mermaid" });
wrapMermaidDiagrams();
}
function wrapMermaidDiagrams() {
document.querySelectorAll(".mermaid").forEach((el) => {
if (el.closest(".mermaid-container")) return;
const container = document.createElement("div");
container.className = "mermaid-container";
const controls = document.createElement("div");
controls.className = "mermaid-zoom-controls";
controls.innerHTML = `
`;
el.replaceWith(container);
container.appendChild(controls);
container.appendChild(el);
let scale = 1;
const setScale = (next) => {
const svg = container.querySelector(".mermaid svg");
if (!svg) return;
scale = Math.max(0.2, Math.min(3, next));
svg.style.transform = `scale(${scale})`;
};
controls.querySelector(".zoom-in").addEventListener("click", () => setScale(scale + 0.1));
controls.querySelector(".zoom-out").addEventListener("click", () => setScale(scale - 0.1));
container.addEventListener("wheel", (e) => {
if (!e.ctrlKey) return;
e.preventDefault();
setScale(scale + (e.deltaY < 0 ? 0.05 : -0.05));
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", renderMermaid);
} else {
renderMermaid();
}
})();