Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
163 lines
5.3 KiB
JavaScript
163 lines
5.3 KiB
JavaScript
(() => {
|
|
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 "<li><a href=\"" + href + "\" data-file=\"" + name.replace(/"/g, "") + "\">" + name + "</a></li>";
|
|
}).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 <code>" + name + "</code>. " +
|
|
"Open it on <a href=\"" + forgejoSrc(name) + "\" rel=\"noopener\">Forgejo</a>.";
|
|
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();
|
|
})();
|