(() => {
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();
})();