Spaces:
Running
Running
| (() => { | |
| document.addEventListener("DOMContentLoaded", () => { | |
| // ---- Inject floating logos into .background ---- | |
| const bg = document.querySelector(".background"); | |
| if (bg && !bg.querySelector(".floating-logo")) { | |
| const isPost = !!document.querySelector(".article"); | |
| const logoSrc = isPost ? "../logo.svg" : "logo.svg"; | |
| for (let i = 0; i < 5; i++) { | |
| const img = document.createElement("img"); | |
| img.src = logoSrc; | |
| img.className = "floating-logo"; | |
| img.alt = ""; | |
| img.setAttribute("aria-hidden", "true"); | |
| bg.appendChild(img); | |
| } | |
| } | |
| // ---- Copy buttons on <pre> ---- | |
| document.querySelectorAll("pre").forEach((pre) => { | |
| if (pre.querySelector(".copy-btn")) return; | |
| const code = pre.querySelector("code") || pre; | |
| const btn = document.createElement("button"); | |
| btn.className = "copy-btn"; | |
| btn.textContent = "Copy"; | |
| btn.addEventListener("click", async () => { | |
| try { | |
| await navigator.clipboard.writeText(code.innerText); | |
| btn.textContent = "Copied!"; | |
| setTimeout(() => (btn.textContent = "Copy"), 1200); | |
| } catch { | |
| btn.textContent = "Error"; | |
| setTimeout(() => (btn.textContent = "Copy"), 1200); | |
| } | |
| }); | |
| pre.appendChild(btn); | |
| }); | |
| // ---- Article-only features: sidebar + collapsible code ---- | |
| const article = document.querySelector(".article"); | |
| if (!article) return; | |
| // Collect headings for TOC | |
| const headings = article.querySelectorAll("h1, h2, h3"); | |
| const codeBlocks = article.querySelectorAll("pre"); | |
| // Determine filenames from preceding h2 | |
| const codeFiles = []; | |
| codeBlocks.forEach((pre, idx) => { | |
| let name = null; | |
| let el = pre.previousElementSibling; | |
| while (el) { | |
| if (el.tagName === "H2" || el.tagName === "H3") { | |
| const text = el.textContent.trim(); | |
| if (text.match(/\.\w{1,5}$/) || text.match(/\.\w{1,5}\s/)) { | |
| name = text.replace(/\s*\(.*\)/, "").trim(); | |
| } | |
| break; | |
| } | |
| el = el.previousElementSibling; | |
| } | |
| if (!name) name = `code-block-${idx + 1}.txt`; | |
| codeFiles.push({ name, pre }); | |
| }); | |
| // Make code blocks collapsible | |
| codeBlocks.forEach((pre, idx) => { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "code-collapsible"; | |
| const toggle = document.createElement("button"); | |
| toggle.className = "code-toggle"; | |
| const fileName = codeFiles[idx].name; | |
| toggle.innerHTML = `<span class="code-toggle-arrow">▶</span> <span class="code-toggle-name">${fileName}</span>`; | |
| toggle.setAttribute("aria-expanded", "false"); | |
| pre.parentNode.insertBefore(wrapper, pre); | |
| wrapper.appendChild(toggle); | |
| wrapper.appendChild(pre); | |
| pre.classList.add("collapsed"); | |
| toggle.addEventListener("click", () => { | |
| const expanded = pre.classList.toggle("collapsed"); | |
| toggle.setAttribute("aria-expanded", !expanded); | |
| toggle.querySelector(".code-toggle-arrow").textContent = expanded ? "▶" : "▼"; | |
| }); | |
| }); | |
| // Build sidebar | |
| const sidebar = document.createElement("nav"); | |
| sidebar.className = "article-sidebar"; | |
| // TOC section | |
| const tocTitle = document.createElement("div"); | |
| tocTitle.className = "sidebar-title"; | |
| tocTitle.textContent = "Contents"; | |
| sidebar.appendChild(tocTitle); | |
| const tocList = document.createElement("ul"); | |
| tocList.className = "sidebar-toc"; | |
| headings.forEach((h, i) => { | |
| if (!h.id) h.id = "heading-" + i; | |
| const li = document.createElement("li"); | |
| li.className = "toc-" + h.tagName.toLowerCase(); | |
| const a = document.createElement("a"); | |
| a.href = "#" + h.id; | |
| a.textContent = h.textContent; | |
| a.addEventListener("click", (e) => { | |
| e.preventDefault(); | |
| h.scrollIntoView({ behavior: "smooth", block: "start" }); | |
| }); | |
| li.appendChild(a); | |
| tocList.appendChild(li); | |
| }); | |
| sidebar.appendChild(tocList); | |
| // Files section | |
| if (codeFiles.length > 0) { | |
| const filesTitle = document.createElement("div"); | |
| filesTitle.className = "sidebar-title"; | |
| filesTitle.textContent = "Files"; | |
| sidebar.appendChild(filesTitle); | |
| const fileList = document.createElement("ul"); | |
| fileList.className = "sidebar-files"; | |
| codeFiles.forEach(({ name, pre }) => { | |
| const li = document.createElement("li"); | |
| const btn = document.createElement("button"); | |
| btn.className = "sidebar-file-btn"; | |
| btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> ${name}`; | |
| btn.addEventListener("click", () => { | |
| const code = pre.querySelector("code") || pre; | |
| const text = code.innerText; | |
| const blob = new Blob([text], { type: "text/plain" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = name; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| li.appendChild(btn); | |
| fileList.appendChild(li); | |
| }); | |
| sidebar.appendChild(fileList); | |
| } | |
| document.body.appendChild(sidebar); | |
| // Highlight active TOC item on scroll | |
| const tocLinks = tocList.querySelectorAll("a"); | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| entries.forEach((entry) => { | |
| if (entry.isIntersecting) { | |
| tocLinks.forEach((l) => l.classList.remove("active")); | |
| const link = tocList.querySelector(`a[href="#${entry.target.id}"]`); | |
| if (link) link.classList.add("active"); | |
| } | |
| }); | |
| }, | |
| { rootMargin: "-80px 0px -70% 0px" } | |
| ); | |
| headings.forEach((h) => observer.observe(h)); | |
| }); | |
| })(); | |