/* SPDX-License-Identifier: GPL-2.0-or-later
*
* The documentation reader.
*
* Renders the project's Markdown in the site's own theme, so the docs can be
* read without leaving the page and without a build step. No dependency: the
* parser below handles exactly what docs/ actually uses -- headings, fenced
* code, tables, lists, quotes, and inline marks. Pulling in a 40 KB Markdown
* library to render 30 files would outweigh every other asset here combined.
*
* Everything is escaped before any tag is emitted. The only HTML that reaches
* the page is HTML this file wrote.
*/
(function () {
"use strict";
/* ---- the index ---------------------------------------------------- */
var DOCS = [
{ group: "Start here", items: [
["README.md", "Overview"],
["plan/1.0.md", "The 1.0 definition"],
["licensing.md", "Licensing"]
]},
{ group: "Architecture", items: [
["architecture/README.md", "The system, layer by layer"],
["architecture/boot.md", "Boot"],
["architecture/kernel.md", "Kernel"],
["architecture/scheduler.md", "Scheduler"],
["architecture/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"],
["architecture/fat.md", "FAT"],
["architecture/terminals.md", "Virtual terminals"],
["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"],
["architecture/graphics.md", "Graphics"],
["architecture/audio.md", "Audio"],
["architecture/networking.md", "Networking"],
["architecture/init.md", "init"],
["architecture/ldls.md", "Linux drivers (LDLS)"],
["architecture/debugging.md", "Debugging"]
]},
{ group: "Guides", items: [
["guides/building.md", "Building"],
["guides/testing.md", "Testing"],
["guides/layout.md", "Tree layout"],
["guides/adding-a-driver.md", "Adding a driver"],
["guides/hardware-report.md", "Reporting hardware"]
]},
{ group: "Plan", items: [
["plan/README.md", "What plan/ is for"],
["plan/roadmap.md", "Roadmap"],
["plan/foundations.md", "Foundations"],
["plan/wayland.md", "Wayland"],
["plan/hybrid.md", "Hybrid kernel"],
["plan/schapi.md", "SchAPI"]
]},
{ group: "History", items: [
["history/README.md", "Why history/ exists"],
["history/kernel.md", "Kernel"],
["history/schfs.md", "SchFS"],
["history/moving-to-linux.md", "Moving to Linux"]
]},
{ group: "Branding", items: [
["branding/README.md", "Shift, and the wallpapers"]
]}
];
/* ---- helpers ------------------------------------------------------- */
function esc(s) {
return s.replace(/&/g, "&").replace(//g, ">");
}
/* Resolve a doc-relative link against the doc it appeared in, so
"../history/schfs.md" inside architecture/ lands on history/schfs.md. */
function resolve(from, href) {
var base = from.split("/").slice(0, -1);
var parts = href.split("/");
for (var i = 0; i < parts.length; i++) {
if (parts[i] === "." || parts[i] === "") continue;
if (parts[i] === "..") base.pop();
else base.push(parts[i]);
}
return base.join("/");
}
/* encodeURIComponent turns every "/" into %2F, which works but makes a
shared link unreadable. Slashes are legal in a query value, so keep them. */
function docHref(p) {
return "?doc=" + encodeURIComponent(p).replace(/%2F/g, "/");
}
function slug(s) {
return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
}
/* ---- inline marks --------------------------------------------------- */
/* U+0001 as the placeholder sentinel: it cannot occur in a Markdown source
file, so a code span lifted out here can never be confused with document
text when it is put back. */
var SENTINEL = "\u0001";
function inline(s, from) {
/* Code spans come out first and go back last, so nothing inside a backtick
pair is mistaken for emphasis or for a link. */
var spans = [];
s = s.replace(/`([^`]+)`/g, function (_, code) {
spans.push("" + esc(code) + "");
return SENTINEL + (spans.length - 1) + SENTINEL;
});
s = esc(s);
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, function (_, alt, src) {
return '';
});
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, function (_, text, href) {
if (/^https?:/.test(href))
return '' + text + "";
if (href.charAt(0) === "#")
return '' + text + "";
if (/\.md($|#)/.test(href)) {
var cut = href.indexOf("#");
var hash = cut >= 0 ? href.slice(cut) : "";
var path = cut >= 0 ? href.slice(0, cut) : href;
return '' + text + "";
}
/* Anything else is a file in the repo rather than a document. Link at it
relative to docs/, which is where this page's ../ points. */
return '' + text + "";
});
s = s.replace(/\*\*([^*]+)\*\*/g, "$1");
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2");
var restore = new RegExp(SENTINEL + "(\\d+)" + SENTINEL, "g");
return s.replace(restore, function (_, n) { return spans[+n]; });
}
/* ---- blocks --------------------------------------------------------- */
function render(md, from, headings) {
md = md.replace(/\r\n?/g, "\n").replace(//g, "");
var L = md.split("\n"), out = [], i = 0, m;
function listBlock(ordered) {
var tag = ordered ? "ol" : "ul", items = [];
while (i < L.length) {
var li = L[i].match(ordered ? /^\s*\d+\.\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/);
if (!li) {
/* an indented continuation line belongs to the item above it */
if (items.length && /^\s+\S/.test(L[i])) {
items[items.length - 1] += " " + L[i].trim();
i++;
continue;
}
break;
}
items.push(li[1]);
i++;
}
out.push("<" + tag + ">");
items.forEach(function (t) { out.push("
" + esc(buf.join("\n")) + "| " + inline(c, from) + " | "); }); out.push("
|---|
| " + inline(c, from) + " | "); }); out.push("
" + render(q.join("\n"), from, []) + ""); continue; } if (/^\s*[-*+]\s+/.test(line)) { listBlock(false); continue; } if (/^\s*\d+\.\s+/.test(line)) { listBlock(true); continue; } /* paragraph: runs to the next blank line or block opener */ var para = []; while (i < L.length && L[i].trim() && !/^\s*(```|>|#{1,6}\s|[-*+]\s|\d+\.\s|\|)/.test(L[i]) && !/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(L[i])) { para.push(L[i++]); } if (para.length) out.push("
" + inline(para.join(" "), from) + "
"); else i++; /* a block opener we do not handle: never spin on it */ } return out.join("\n"); } /* ---- the page ------------------------------------------------------- */ var nav = document.getElementById("docnav"); var article = document.getElementById("doc"); var tocEl = document.getElementById("toc"); function buildNav() { DOCS.forEach(function (g) { var h = document.createElement("p"); h.className = "nv-nav__section"; h.textContent = g.group; nav.appendChild(h); var ul = document.createElement("ul"); ul.className = "nv-nav__list"; g.items.forEach(function (it) { var li = document.createElement("li"); var a = document.createElement("a"); a.className = "nv-nav__item"; a.href = docHref(it[0]); a.textContent = it[1]; a.dataset.path = it[0]; li.appendChild(a); ul.appendChild(li); }); nav.appendChild(ul); }); } function markCurrent(path) { Array.prototype.forEach.call(nav.querySelectorAll("a"), function (a) { if (a.dataset.path === path) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current"); }); } /* A heading's own Markdown, as text. The contents panel is a list of destinations, not a place to render code and emphasis -- a heading written `proc_reincarnate()` should read as proc_reincarnate() there rather than carrying its backticks across. Marks are stripped, not escaped: the result goes through textContent. */ function plain(s) { return s.replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\*([^*]+)\*/g, "$1") .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1"); } function buildToc(headings) { tocEl.innerHTML = ""; var real = headings.filter(function (h) { return h.level >= 2; }); if (real.length < 2) { tocEl.hidden = true; return; } tocEl.hidden = false; var t = document.createElement("p"); t.className = "nv-nav__section"; t.textContent = "On this page"; tocEl.appendChild(t); var ul = document.createElement("ul"); ul.className = "nv-nav__list"; real.forEach(function (h) { var li = document.createElement("li"); li.className = "toc-lvl" + h.level; var a = document.createElement("a"); a.className = "nv-nav__item"; a.href = "#" + h.id; a.textContent = plain(h.text); li.appendChild(a); ul.appendChild(li); }); tocEl.appendChild(ul); } /* Every document in index order, with the group it belongs to. */ var FLAT = []; DOCS.forEach(function (g) { g.items.forEach(function (it) { FLAT.push({ path: it[0], title: it[1], group: g.group }); }); }); function indexOf(path) { for (var k = 0; k < FLAT.length; k++) if (FLAT[k].path === path) return k; return -1; } /* Where the reader is, how long it will take, and where the file lives -- above the document, because a page arrived at from a search has no other way to say which part of the docs it belongs to. */ function header(path, text) { var at = FLAT[indexOf(path)] || { group: "Docs", title: path }; var words = text.split(/\s+/).length; var mins = Math.max(1, Math.round(words / 230)); return 'Browsers block fetch() on file:// URLs, so the " +
"reader cannot load the Markdown. The rest of the site works from a file; " +
"this page does not. Serve the repository root:
cd path/to/SchOS\n' +
"python3 -m http.server 8000" +
"Then open http://localhost:8000/docs/Website/docs.html.
Or read this one directly: ' + esc(path) + "
"); } else { fail("Could not load that document", "" + esc(path) + " — " + esc(e.message) + "