The site and several documents still described the system as it was before virtual terminals, the new schinit, the hybrid work and descriptor passing. docs/architecture/init.md described the wrong program. It documented a single schinit that parses manifests and supervises jobs, which has not been true since PID 1 was split: PID 1 forks a supervisor, spawns jobs on its behalf and reaps; the job table lives in /proc/schinit; a heartbeat runs both ways; and proc_reincarnate() re-execs PID 1 in place rather than panicking. Rewritten around what exists, keeping the reasoning that is still load-bearing -- the five failures of the sibling-init scheme, the escalation ladder, and the three bugs in the reincarnation path that each had to happen before they were obvious. docs/plan/init.md is deleted, which is the tree's own rule for finished work and had not been applied. Fourteen source comments referenced it by part number, so they are repointed at the architecture doc by section name rather than left dangling -- a deleted document nobody notices is worse than a stale one. docs/architecture/devices.md gained the section it has been missing since /dev/pci existed: what a driver needs from a kernel, why mmap is checked against the caller's claims, why DMA is a call and not a mapping, and why the write back after an interrupt is not optional. Also that a released device does not come back by itself, which is true and was nowhere. docs/architecture/networking.md gained a descriptor-passing section, and docs/architecture/README.md's stack diagram gained the two layers and the third seam it was missing. "Two seams hold the whole thing together" has not been true since /dev/pci. docs/plan/README.md said the hybrid work was deliberately last, on the argument that an interface with no consumer is wrong in ways nobody can see. The argument was right; the conclusion was not, and the document now says why it changed -- the alternative to a driver compatibility layer is writing a driver for every device that exists, and the original objection is answered rather than ignored because SchOS's own drivers are the consumers. The site: a Hybrid section with the eviction transcript, cards for virtual terminals, schinit and drivers outside the kernel, and the numbers remeasured rather than estimated -- 46,961 lines, 207 syscalls. The boot log in app.js is from a boot taken today, and the lines are trimmed to what the console can show without clipping, each still a verbatim prefix of real output. Two small rendering fixes found by looking at the pages rather than assuming: .console pre's min-height is now scoped to the hero, so a static transcript sizes to its content instead of reserving eight lines it does not have; and the docs reader's contents panel strips inline marks from headings, so a heading written `proc_reincarnate()` no longer carries its backticks into the sidebar. Every intra-doc link checked; none broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
374 lines
14 KiB
JavaScript
374 lines
14 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 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/syscalls.md", "Syscalls"],
|
|
["architecture/filesystem.md", "The VFS"],
|
|
["architecture/schfs.md", "SchFS"],
|
|
["architecture/terminals.md", "Virtual terminals"],
|
|
["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/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, "&").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("<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);
|
|
});
|
|
}
|
|
|
|
/* 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 = "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 = plain(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()); });
|
|
})();
|