(() => { "use strict"; const embedded = JSON.parse(document.getElementById("docslib-manifest").textContent); const config = JSON.parse(document.getElementById("docslib-config").textContent); const elements = { title: document.getElementById("site-title"), tree: document.getElementById("page-tree"), content: document.getElementById("content"), breadcrumbs: document.getElementById("breadcrumbs"), previous: document.getElementById("previous-page"), next: document.getElementById("next-page"), status: document.getElementById("update-status"), searchDialog: document.getElementById("search-dialog"), searchInput: document.getElementById("search-input"), searchResults: document.getElementById("search-results"), themeIcon: document.getElementById("theme-icon"), headerButtons: document.getElementById("header-buttons"), headerActions: document.getElementById("header-actions"), headerActionsAnchor: document.getElementById("header-actions-anchor"), openActions: document.getElementById("open-actions"), workspace: document.querySelector(".workspace"), }; let manifest = embedded; let currentNumber = null; const mobileLayout = matchMedia("(max-width: 800px)"); const dbPromise = new Promise((resolve, reject) => { if (!window.indexedDB) return reject(new Error("IndexedDB unavailable")); const request = indexedDB.open("docslib-cache", 1); request.onupgradeneeded = () => request.result.createObjectStore("manifests"); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); async function cacheGet() { const db = await dbPromise; return new Promise((resolve, reject) => { const request = db.transaction("manifests", "readonly").objectStore("manifests").get(config.cacheKey); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function cachePut(value) { const db = await dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction("manifests", "readwrite"); transaction.objectStore("manifests").put(value, config.cacheKey); transaction.oncomplete = resolve; transaction.onerror = () => reject(transaction.error); }); } function manifestIdentity(value = manifest) { const updated = String(value.updated_at || "") .replace("T", " ") .replace(/\.\d+(?=Z|[+-])/, "") .replace("+00:00", "Z"); const revision = String(value.revision || "").slice(0, 8); return [updated, revision].filter(Boolean).join(" · ") || "—"; } function setStatus(state = "", detail = "") { elements.status.className = `update-status ${state}`.trim(); elements.status.querySelector("span").textContent = manifestIdentity(); elements.status.title = detail || String(manifest.revision || ""); } function routeNumber() { const match = location.hash.match(/^#\/page\/(.+)$/); return match ? decodeURIComponent(match[1]) : null; } function navigate(number, replace = false) { const hash = `#/page/${encodeURIComponent(number)}`; if (replace) history.replaceState(null, "", hash); else if (location.hash !== hash) location.hash = hash; renderPage(number); document.body.classList.remove("sidebar-open"); document.body.classList.remove("actions-open"); } function placeHeaderActions() { if (mobileLayout.matches) { if (elements.headerActions.parentElement !== document.body) document.body.append(elements.headerActions); } else { if (elements.headerActions.previousElementSibling !== elements.headerActionsAnchor) { elements.headerActionsAnchor.after(elements.headerActions); } document.body.classList.remove("actions-open"); } } function makeTree() { elements.tree.replaceChildren(); const root = { children: new Map(), page: null }; manifest.pages.forEach((page) => { let node = root; page.number.split(".").forEach((part) => { if (!node.children.has(part)) node.children.set(part, { children: new Map(), page: null }); node = node.children.get(part); }); node.page = page; }); const appendNodes = (parent, node) => { node.children.forEach((child) => { const group = document.createElement("div"); group.className = "tree-group"; if (child.page) { const button = document.createElement("button"); button.className = "tree-link"; button.dataset.page = child.page.number; const number = document.createElement("span"); number.className = "tree-number"; number.textContent = child.page.number; const title = document.createElement("span"); title.className = "tree-title"; title.textContent = child.page.title; button.append(number, title); button.addEventListener("click", () => navigate(child.page.number)); group.append(button); } if (child.children.size) { const nested = document.createElement("div"); nested.className = "tree-children"; appendNodes(nested, child); group.append(nested); } parent.append(group); }); }; appendNodes(elements.tree, root); } function assetDataUri(name) { const asset = manifest.assets && manifest.assets[name]; return asset ? `data:${asset.mime};base64,${asset.data}` : ""; } function hydrateAssets() { elements.content.querySelectorAll("[data-docslib-asset]").forEach((element) => { const name = element.dataset.docslibAsset; const uri = assetDataUri(name); if (uri && element.tagName === "IMG") element.src = uri; else if (!uri) { element.setAttribute("title", name); element.classList.add("missing-asset"); } }); } function renderPage(number) { if (!manifest.pages.length) { elements.content.replaceChildren(); elements.previous.disabled = elements.next.disabled = true; return; } let index = manifest.pages.findIndex((page) => page.number === number); if (index < 0) index = 0; const page = manifest.pages[index]; currentNumber = page.number; if (routeNumber() !== page.number) history.replaceState(null, "", `#/page/${encodeURIComponent(page.number)}`); document.title = `${page.title} · ${manifest.title}`; elements.content.innerHTML = page.content || ""; hydrateAssets(); elements.content.querySelectorAll("a[href^='#/page/']").forEach((link) => { link.addEventListener("click", (event) => { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); navigate(decodeURIComponent(link.hash.slice(7))); }); }); elements.tree.querySelectorAll(".tree-link").forEach((button) => button.classList.toggle("active", button.dataset.page === page.number)); elements.headerButtons.querySelectorAll("[data-page]").forEach((button) => { const target = button.dataset.page; button.classList.toggle("active", page.number === target || page.number.startsWith(`${target}.`)); }); elements.breadcrumbs.replaceChildren(); const numberParts = page.number.split("."); numberParts.forEach((_, partIndex) => { const candidate = manifest.pages.find((item) => item.number === numberParts.slice(0, partIndex + 1).join(".")); if (candidate) { const crumb = document.createElement("span"); crumb.textContent = candidate.title; elements.breadcrumbs.append(crumb); } }); setupNavButton(elements.previous, manifest.pages[index - 1], "←"); setupNavButton(elements.next, manifest.pages[index + 1], "→"); elements.content.focus({ preventScroll: true }); elements.workspace.scrollTo({ top: 0, behavior: "instant" }); } function setupNavButton(button, page, direction) { button.disabled = !page; button.replaceChildren(); if (!page) return; const small = document.createElement("small"); small.textContent = direction; const title = document.createElement("span"); title.textContent = page.title; button.append(small, title); button.onclick = () => navigate(page.number); } function renderHeaderButtons() { elements.headerButtons.replaceChildren(); const buttons = (manifest.ui && manifest.ui.header_buttons) || []; buttons.forEach((button) => { if (!button.label || (!button.href && !button.page)) return; const link = document.createElement("a"); link.className = "header-link"; link.textContent = button.label; if (button.page) { link.href = `#/page/${encodeURIComponent(button.page)}`; link.dataset.page = button.page; link.addEventListener("click", (event) => { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); navigate(button.page); }); } else { link.href = button.href; } if (!button.page && button.target === "_blank") { link.target = "_blank"; link.rel = "noopener noreferrer"; } link.addEventListener("click", () => document.body.classList.remove("actions-open")); elements.headerButtons.append(link); }); elements.openActions.hidden = buttons.length === 0; } function renderAll() { document.documentElement.lang = "und"; elements.title.textContent = manifest.title; makeTree(); renderHeaderButtons(); renderPage(routeNumber() || currentNumber || (manifest.pages[0] && manifest.pages[0].number)); setStatus(); } function plainText(html) { const node = document.createElement("div"); node.innerHTML = html; return (node.textContent || "").replace(/\s+/g, " ").trim(); } function search(query) { const needle = query.trim().toLocaleLowerCase(); const matches = !needle ? manifest.pages : manifest.pages.filter((page) => `${page.number} ${page.title} ${plainText(page.content)}`.toLocaleLowerCase().includes(needle) ); elements.searchResults.replaceChildren(); matches.slice(0, 30).forEach((page) => { const button = document.createElement("button"); button.className = "search-result"; const title = document.createElement("strong"); title.textContent = `${page.number} ${page.title}`; const excerpt = document.createElement("span"); excerpt.textContent = plainText(page.content).slice(0, 150) || "—"; button.append(title, excerpt); button.addEventListener("click", () => { elements.searchDialog.close(); navigate(page.number); }); elements.searchResults.append(button); }); if (!matches.length) elements.searchResults.innerHTML = '