(() => { const OWNER = "nulldrm"; const REPO = "Null-DRM-Official"; const BRANCH = "main"; const API = "/git-api/api/v1/repos/" + OWNER + "/" + REPO; const FORGEJO = "https://git.nulldrm.com/" + OWNER + "/" + REPO; const DEFAULT_DOC = "README.md"; const FALLBACK = [ "README.md", "quickstart.md", "architecture.md", "modules.md", "capture.md", "streamd.md", "providers/rte.md", "providers/tg4.md", "providers/bbc.md", ]; // Order the nav the way the docs are meant to be read. const ORDER = FALLBACK; const listEl = document.getElementById("docs-list"); const contentEl = document.getElementById("docs-content"); const statusEl = document.getElementById("docs-status"); const titleEl = document.getElementById("docs-title"); const sourceEl = document.getElementById("docs-source"); function currentFile() { const params = new URLSearchParams(window.location.search); const f = (params.get("f") || DEFAULT_DOC).trim(); // Allow one level of subdirectory (providers/tg4.md) but nothing escaping docs/. if (!f || f.includes("..") || f.includes("\\")) return DEFAULT_DOC; if (!/^[\w.-]+(?:\/[\w.-]+)?\.md$/i.test(f)) return DEFAULT_DOC; return f; } // Keep "/" readable in the path; only the segments need escaping. function encodePath(name) { return name.split("/").map(encodeURIComponent).join("/"); } function forgejoSrc(name) { return FORGEJO + "/src/branch/" + BRANCH + "/docs/" + encodePath(name); } function forgejoRaw(name) { return "/git-api/" + OWNER + "/" + REPO + "/raw/branch/" + BRANCH + "/docs/" + encodePath(name); } function setActiveNav(name) { if (!listEl) return; listEl.querySelectorAll("a").forEach((a) => { a.classList.toggle("active", a.getAttribute("data-file") === name); }); } function renderNav(files) { if (!listEl) return; listEl.innerHTML = files.map((name) => { const href = "docs.html?f=" + encodeURIComponent(name); return "
  • " + name + "
  • "; }).join(""); } function rewriteDocLinks(root) { root.querySelectorAll("a[href]").forEach((a) => { const href = a.getAttribute("href") || ""; if (/^https?:\/\//i.test(href) || href.startsWith("#") || href.startsWith("mailto:")) return; const clean = href.replace(/^\.\//, ""); if (/^[\w.-]+(?:\/[\w.-]+)?\.md(?:#.*)?$/i.test(clean)) { const [file, hash] = clean.split("#"); a.setAttribute("href", "docs.html?f=" + encodeURIComponent(file) + (hash ? "#" + hash : "")); } }); } async function dirDocs(path) { const res = await fetch(API + "/contents/" + path + "?ref=" + encodeURIComponent(BRANCH)); if (!res.ok) throw new Error("HTTP " + res.status); const entries = await res.json(); return Array.isArray(entries) ? entries : []; } async function listDocs() { try { const entries = await dirDocs("docs"); const files = entries .filter((e) => e && e.type === "file" && /\.md$/i.test(e.name || "")) .map((e) => e.name); // Pull in one level of subdirectories, e.g. docs/providers/. const dirs = entries.filter((e) => e && e.type === "dir" && e.name); for (const d of dirs) { try { const sub = await dirDocs("docs/" + d.name); sub .filter((e) => e && e.type === "file" && /\.md$/i.test(e.name || "")) .forEach((e) => files.push(d.name + "/" + e.name)); } catch { /* a subdirectory we cannot list is not fatal */ } } files.sort((a, b) => { const ia = ORDER.indexOf(a); const ib = ORDER.indexOf(b); if (ia !== -1 || ib !== -1) { return (ia === -1 ? ORDER.length : ia) - (ib === -1 ? ORDER.length : ib); } return a.localeCompare(b); }); return files.length ? files : FALLBACK; } catch { return FALLBACK; } } async function loadDoc(name) { if (titleEl) titleEl.textContent = name; if (sourceEl) { sourceEl.href = forgejoSrc(name); sourceEl.textContent = "View on Forgejo"; } setActiveNav(name); if (statusEl) { statusEl.hidden = false; statusEl.textContent = "Loading " + name + "…"; } if (contentEl) contentEl.hidden = true; try { const res = await fetch(forgejoRaw(name)); if (!res.ok) throw new Error("HTTP " + res.status); const md = await res.text(); if (typeof marked === "undefined" || !marked.parse) { throw new Error("marked missing"); } marked.setOptions({ gfm: true, breaks: false }); contentEl.innerHTML = marked.parse(md); rewriteDocLinks(contentEl); statusEl.hidden = true; contentEl.hidden = false; document.title = name + " — NULLDRM Docs"; } catch { if (statusEl) { statusEl.innerHTML = "Could not load " + name + ". " + "Open it on Forgejo."; statusEl.hidden = false; } if (contentEl) contentEl.hidden = true; } } async function boot() { const files = await listDocs(); renderNav(files); let file = currentFile(); if (files.indexOf(file) === -1) file = files[0] || DEFAULT_DOC; await loadDoc(file); } boot(); })();