SchOS/docs/Website/docs.js
Nexoniarz ef94fac4ac docs: a Markdown reader on the site, and the flag stays up
Two additions to docs/Website.

**The reader.** docs.html renders the 29 documents in docs/ in the site's own
theme -- index down the left, table of contents down the right, and links
between documents rewritten so ../history/schfs.md navigates inside the
reader instead of downloading a file.

The parser is ~150 lines in docs.js, not a dependency: it handles exactly
what docs/ uses, and a Markdown library would outweigh every other asset here
combined. Everything is escaped before a tag is emitted, so the only HTML
that reaches the page is HTML docs.js wrote. Verified by rendering all 30
documents (302 KB of Markdown) with zero failures, no unrestored sentinels
and no runaway loops.

One caveat, stated in the page itself rather than hidden: browsers block
fetch() on file:// URLs, so this page needs a server. It says so, with the
command, and offers the raw file as a fallback. The rest of the site is
unaffected.

**The flag.** A Progress Pride flag in the top bar and a line in the footer,
on both pages, permanently. Support with an end date is marketing. It is
deliberately small and deliberately not a page of its own -- this is an
operating system, and the flag is a statement, not a subject.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 21:53:06 +02:00

362 lines
13 KiB
JavaScript

/* SPDX-License-Identifier: Apache-2.0
*
* 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 29 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/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"],
["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"],
["architecture/graphics.md", "Graphics"],
["architecture/networking.md", "Networking"],
["architecture/init.md", "init"]
]},
{ group: "Guides", items: [
["guides/building.md", "Building"],
["guides/testing.md", "Testing"],
["guides/layout.md", "Tree layout"],
["guides/adding-a-driver.md", "Adding a driver"]
]},
{ group: "Plan", items: [
["plan/README.md", "What plan/ is for"],
["plan/roadmap.md", "Roadmap"],
["plan/foundations.md", "Foundations"],
["plan/init.md", "schinit"],
["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", "The three marks"]
]}
];
/* ---- helpers ------------------------------------------------------- */
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
/* 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("<code>" + esc(code) + "</code>");
return SENTINEL + (spans.length - 1) + SENTINEL;
});
s = esc(s);
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, function (_, alt, src) {
return '<img src="' + encodeURI(src) + '" alt="' + alt + '">';
});
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, function (_, text, href) {
if (/^https?:/.test(href))
return '<a href="' + encodeURI(href) + '" target="_blank" rel="noopener">' + text + "</a>";
if (href.charAt(0) === "#")
return '<a href="' + encodeURI(href) + '">' + text + "</a>";
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 '<a href="' + docHref(resolve(from, path)) + hash + '">' + text + "</a>";
}
/* 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 '<a href="../' + encodeURI(resolve(from, href)) + '">' + text + "</a>";
});
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
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(/<!--[\s\S]*?-->/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("<li>" + inline(t, from) + "</li>"); });
out.push("</" + tag + ">");
}
function cells(row) {
return row.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|")
.map(function (c) { return c.trim(); });
}
while (i < L.length) {
var line = L[i];
if (!line.trim()) { i++; continue; }
/* fenced code */
if (/^\s*```/.test(line)) {
var lang = line.replace(/^\s*```/, "").trim(), buf = [];
i++;
while (i < L.length && !/^\s*```/.test(L[i])) buf.push(L[i++]);
i++;
out.push('<pre class="code"' + (lang ? ' data-lang="' + esc(lang) + '"' : "") +
"><code>" + esc(buf.join("\n")) + "</code></pre>");
continue;
}
/* table: a pipe row followed by a separator row */
if (/^\s*\|/.test(line) && /^\s*\|?[\s:|-]+$/.test(L[i + 1] || "") && /-/.test(L[i + 1] || "")) {
var head = cells(line);
i += 2;
out.push('<div class="tablewrap"><table><thead><tr>');
head.forEach(function (c) { out.push("<th>" + inline(c, from) + "</th>"); });
out.push("</tr></thead><tbody>");
while (i < L.length && /^\s*\|/.test(L[i])) {
out.push("<tr>");
cells(L[i++]).forEach(function (c) { out.push("<td>" + inline(c, from) + "</td>"); });
out.push("</tr>");
}
out.push("</tbody></table></div>");
continue;
}
/* heading */
if ((m = line.match(/^(#{1,6})\s+(.*?)\s*#*$/))) {
var lvl = m[1].length, text = m[2], id = slug(text);
if (lvl <= 3) headings.push({ level: lvl, text: text, id: id });
out.push("<h" + lvl + ' id="' + id + '">' + inline(text, from) + "</h" + lvl + ">");
i++;
continue;
}
/* rule */
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("<hr>"); i++; continue; }
/* blockquote */
if (/^\s*>/.test(line)) {
var q = [];
while (i < L.length && /^\s*>/.test(L[i])) q.push(L[i++].replace(/^\s*>\s?/, ""));
out.push("<blockquote>" + render(q.join("\n"), from, []) + "</blockquote>");
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("<p>" + inline(para.join(" "), from) + "</p>");
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 = "navgroup";
h.textContent = g.group;
nav.appendChild(h);
var ul = document.createElement("ul");
g.items.forEach(function (it) {
var li = document.createElement("li");
var a = document.createElement("a");
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) {
a.classList.toggle("on", a.dataset.path === path);
});
}
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 = "navgroup";
t.textContent = "On this page";
tocEl.appendChild(t);
var ul = document.createElement("ul");
real.forEach(function (h) {
var li = document.createElement("li");
li.className = "lvl" + h.level;
var a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.text;
li.appendChild(a);
ul.appendChild(li);
});
tocEl.appendChild(ul);
}
function fail(title, body) {
article.innerHTML = "<h1>" + title + "</h1>" + body;
tocEl.hidden = true;
}
function load(path) {
markCurrent(path);
document.title = "Docs — SchOS";
article.setAttribute("aria-busy", "true");
fetch("../" + path).then(function (r) {
if (!r.ok) throw new Error(r.status + " " + r.statusText);
return r.text();
}).then(function (text) {
var headings = [];
article.innerHTML = render(text, path, headings);
buildToc(headings);
article.removeAttribute("aria-busy");
if (headings.length) document.title = headings[0].text + " — SchOS docs";
var hash = location.hash.slice(1);
var target = hash && document.getElementById(hash);
if (target) target.scrollIntoView();
else window.scrollTo(0, 0);
}).catch(function (e) {
article.removeAttribute("aria-busy");
/* The common case, and worth naming precisely: browsers refuse fetch()
on file:// for the same reason they refuse it cross-origin, so this
one page needs a server even though nothing here is dynamic. */
if (location.protocol === "file:") {
fail("This page needs a local server",
"<p>Browsers block <code>fetch()</code> on <code>file://</code> 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:</p>" +
'<pre class="code"><code>cd /home/nexoniarz/Dokumenty/Projects/SchOS\n' +
"python3 -m http.server 8000</code></pre>" +
"<p>Then open <code>http://localhost:8000/docs/Website/docs.html</code>.</p>" +
'<p>Or read this one directly: <a href="../' + esc(path) + '">' + esc(path) + "</a></p>");
} else {
fail("Could not load that document",
"<p><code>" + esc(path) + "</code> — " + esc(e.message) + "</p>" +
'<p><a href="?doc=README.md">Back to the overview</a></p>');
}
});
}
function pathFromUrl() {
var m = /[?&]doc=([^&#]+)/.exec(location.search);
var p = m ? decodeURIComponent(m[1]) : "README.md";
/* Only ever load something that is in the index above. */
var known = DOCS.some(function (g) {
return g.items.some(function (it) { return it[0] === p; });
});
return known ? p : "README.md";
}
buildNav();
load(pathFromUrl());
/* Intercept in-page navigation so the reader does not reload its own shell. */
document.addEventListener("click", function (ev) {
var a = ev.target.closest ? ev.target.closest("a") : null;
if (!a || ev.metaKey || ev.ctrlKey || ev.shiftKey || a.target) return;
var href = a.getAttribute("href") || "";
if (href.indexOf("?doc=") !== 0) return;
ev.preventDefault();
history.pushState(null, "", href);
load(pathFromUrl());
});
window.addEventListener("popstate", function () { load(pathFromUrl()); });
})();