diff --git a/docs/Website/Assets/pride.svg b/docs/Website/Assets/pride.svg new file mode 100644 index 0000000..be1b140 --- /dev/null +++ b/docs/Website/Assets/pride.svg @@ -0,0 +1,22 @@ + + Progress Pride flag + The six-stripe rainbow with the Progress chevron: white, pink and light + blue for trans people, brown and black for queer people of colour and for + those lost to AIDS. It sits in the top bar every day of the year, which is + the whole point of it. + + + + + + + + + + + + + + + + diff --git a/docs/Website/README.md b/docs/Website/README.md index 4007a92..c67ad36 100644 --- a/docs/Website/README.md +++ b/docs/Website/README.md @@ -5,12 +5,49 @@ Three files and a folder of marks. No build step, no dependencies, no framework — open `index.html` and it works, including from a `file://` URL. ``` -index.html the page +index.html the front page +docs.html the documentation reader style.css the palette, transcribed from include/theme.h -app.js two behaviours: the boot log types, sections fade in -Assets/ shift.svg (the logo), uberblock.svg (SchFS), guru.svg (PSOGM) +app.js the front page: the boot log types, sections fade in +docs.js the Markdown renderer and the doc index +Assets/ shift.svg (logo), uberblock.svg (SchFS), guru.svg (PSOGM), + pride.svg ``` +## The docs reader + +`docs.html` renders the Markdown in `docs/` in the site's own theme, with the +index down the left and a table of contents down the right. Links between +documents are rewritten to stay inside the reader, so `../history/schfs.md` +in an architecture page navigates rather than downloads. + +The parser is about 150 lines in `docs.js` and handles what `docs/` actually +uses: headings, fenced code, tables, lists, quotes, rules and inline marks. +A Markdown library would be larger than everything else here put together. +Every document is escaped before a single tag is emitted — the only HTML that +reaches the page is HTML `docs.js` wrote. + +**This one page needs a server.** Browsers refuse `fetch()` on `file://` for +the same reason they refuse it cross-origin, so opening `docs.html` from a +file gives a panel explaining that instead of the docs. Everything else works +from a file. To read them locally: + +```sh +cd /home/nexoniarz/Dokumenty/Projects/SchOS +python3 -m http.server 8000 +# then http://localhost:8000/docs/Website/docs.html +``` + +Adding a document means adding one line to the `DOCS` array in `docs.js`. The +reader will only load a path that appears there. + +## The flag + +`Assets/pride.svg` is in the top bar and in the footer, on both pages, and it +is not seasonal. Support with an end date is marketing; this is a project that +belongs to whoever writes it. It is also deliberately small — the site is +about an operating system, and the flag is a statement, not a subject. + ## The rules it follows **The palette is not a design decision.** It is `include/theme.h`, copied. A diff --git a/docs/Website/docs.html b/docs/Website/docs.html new file mode 100644 index 0000000..77a0481 --- /dev/null +++ b/docs/Website/docs.html @@ -0,0 +1,63 @@ + + + + + +Docs — SchOS + + + + + + +
+
+ + + SchOS + + + Progress Pride flag + + +
+
+ +
+ + +
+
+

Loading…

+
+
+ + +
+ + + + + + diff --git a/docs/Website/docs.js b/docs/Website/docs.js new file mode 100644 index 0000000..10061d8 --- /dev/null +++ b/docs/Website/docs.js @@ -0,0 +1,362 @@ +/* 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, "&").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 '' + alt + ''; + }); + + 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("
  • " + inline(t, from) + "
  • "); }); + out.push(""); + } + + 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('
    " + esc(buf.join("\n")) + "
    "); + 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('
    '); + head.forEach(function (c) { out.push(""); }); + out.push(""); + while (i < L.length && /^\s*\|/.test(L[i])) { + out.push(""); + cells(L[i++]).forEach(function (c) { out.push(""); }); + out.push(""); + } + out.push("
    " + inline(c, from) + "
    " + inline(c, from) + "
    "); + 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("' + inline(text, from) + ""); + i++; + continue; + } + + /* rule */ + if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("
    "); 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("
    " + 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 = "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 = "

    " + title + "

    " + 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", + "

    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 /home/nexoniarz/Dokumenty/Projects/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) + "

    " + + '

    Back to the overview

    '); + } + }); + } + + 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()); }); +})(); diff --git a/docs/Website/index.html b/docs/Website/index.html index 619b2a6..0239fe0 100644 --- a/docs/Website/index.html +++ b/docs/Website/index.html @@ -16,12 +16,15 @@ SchOS + + Progress Pride flag + @@ -263,17 +266,18 @@ plan/; and the bugs worth remembering are kept in history/, in full, including the wrong turns.

    + @@ -286,6 +290,11 @@
    SchOS — kernel SchIFT, filesystem SchFS. Apache-2.0, with the xv6-derived core under MIT. + + + Built by, and for, people of every orientation and gender identity. + The flag stays up all year. +
    diff --git a/docs/Website/style.css b/docs/Website/style.css index a795708..26f96f4 100644 --- a/docs/Website/style.css +++ b/docs/Website/style.css @@ -369,3 +369,210 @@ footer .mono { font-size: 0.78rem; } vertical-align: -0.22em; margin-right: 0.4em; } + +/* ===================================================================== + The pride mark + --------------------------------------------------------------------- + Small, and permanent. It is in the top bar in November the same way it + is in June, which is the only version of this that means anything -- + support with an end date is marketing. + + Deliberately not bigger than this. The site is about an operating + system; the flag is a statement, not a subject. + ===================================================================== */ + +.pride { + display: inline-flex; + align-items: center; + line-height: 0; + border-radius: 2px; + text-decoration: none; +} +.pride img { + width: 26px; + height: auto; + display: block; + border-radius: 2px; + /* Sits down slightly against the dark ground so it reads as part of the + bar rather than as a sticker on top of it; full colour on hover. */ + opacity: 0.85; + transition: opacity .18s ease, transform .18s ease; +} +.pride:hover img { opacity: 1; transform: translateY(-1px); } +@media (prefers-reduced-motion: reduce) { + .pride img { transition: none; } + .pride:hover img { transform: none; } +} + +.pride-note { + display: flex; + align-items: center; + gap: 0.6rem; + flex-basis: 100%; + color: var(--dim); + font-size: 0.82rem; +} +.pride-note img { width: 20px; height: auto; border-radius: 2px; flex: none; } + +/* ===================================================================== + The documentation reader + ===================================================================== */ + +body.reading { display: flex; flex-direction: column; min-height: 100vh; } +body.reading footer { margin-top: auto; } + +.docshell { + width: min(100% - 2.5rem, 84rem); + margin-inline: auto; + display: grid; + grid-template-columns: 15rem minmax(0, 1fr) 13rem; + gap: clamp(1.5rem, 3vw, 3rem); + align-items: start; + padding-block: 2.5rem 4rem; +} +@media (max-width: 72rem) { .docshell { grid-template-columns: 14rem minmax(0, 1fr); } .rail { display: none; } } +@media (max-width: 54rem) { .docshell { grid-template-columns: 1fr; } .sidebar .sticky { position: static; max-height: none; } } + +.sticky { + position: sticky; + top: 4.5rem; + max-height: calc(100vh - 6rem); + overflow-y: auto; + scrollbar-width: thin; +} + +.navgroup { + font-family: var(--mono); + font-size: 0.68rem; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--dim); + margin: 1.6rem 0 0.5rem; +} +.navgroup:first-child { margin-top: 0; } + +#docnav ul, #toc ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } +#docnav a, #toc a { + display: block; + padding: 0.28rem 0.6rem; + margin-left: -0.6rem; + border-radius: 3px; + color: var(--dim); + text-decoration: none; + font-size: 0.875rem; + line-height: 1.45; +} +#docnav a:hover, #toc a:hover { color: var(--fg); background: var(--surface); } +#docnav a.on { + color: var(--fg); + background: var(--accent-dim); + box-shadow: inset 2px 0 0 var(--accent); +} +#toc .lvl3 a { padding-left: 1.5rem; font-size: 0.8rem; } + +/* ---- prose ---------------------------------------------------------- */ + +.prose { max-width: 46rem; } +.prose .loading { color: var(--dim); font-family: var(--mono); font-size: 0.85rem; } + +.prose h1 { + font-size: clamp(1.9rem, 1.4rem + 2.2vw, 2.6rem); + font-weight: 700; + letter-spacing: -0.025em; + margin: 0 0 1.25rem; +} +.prose h2 { + font-size: clamp(1.3rem, 1.1rem + 1vw, 1.65rem); + font-weight: 700; + letter-spacing: -0.015em; + margin: 2.75rem 0 0.9rem; + padding-top: 1.6rem; + border-top: 1px solid var(--line); +} +.prose h3 { font-size: 1.1rem; font-weight: 700; margin: 2rem 0 0.6rem; } +.prose h4, .prose h5, .prose h6 { font-size: 0.95rem; font-weight: 700; margin: 1.5rem 0 0.5rem; color: var(--dim); } + +.prose p, .prose li { color: #C6CAD3; } +.prose p { margin: 0 0 1.1rem; max-width: none; } +.prose strong { color: var(--fg); } +.prose a { color: var(--accent); } + +.prose ul, .prose ol { margin: 0 0 1.2rem; padding-left: 1.35rem; display: flex; flex-direction: column; gap: 0.4rem; } +.prose li { line-height: 1.6; } +.prose li::marker { color: var(--accent); } + +.prose hr { border: 0; border-top: 1px solid var(--line); margin: 2.25rem 0; } + +.prose blockquote { + margin: 1.4rem 0; + padding: 0.2rem 0 0.2rem 1.1rem; + border-left: 2px solid var(--accent); + color: var(--dim); +} +.prose blockquote p:last-child { margin-bottom: 0; } + +.prose pre.code { + background: #090B0F; + border: 1px solid var(--line); + border-radius: 5px; + padding: 0.95rem 1.1rem; + overflow-x: auto; + margin: 0 0 1.3rem; + position: relative; +} +.prose pre.code code { + font-family: var(--mono); + font-size: 0.815rem; + line-height: 1.7; + color: #CBD1DC; + background: none; + padding: 0; + white-space: pre; +} +.prose pre.code[data-lang]::after { + content: attr(data-lang); + position: absolute; + top: 0.5rem; + right: 0.7rem; + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--dim); +} + +.prose :not(pre) > code { + font-family: var(--mono); + font-size: 0.85em; + background: var(--surface-2); + border: 1px solid var(--line); + color: #D8DCE4; + padding: 0.08em 0.36em; + border-radius: 3px; +} + +.tablewrap { overflow-x: auto; margin: 0 0 1.4rem; } +.prose table { border-collapse: collapse; width: 100%; font-size: 0.9rem; } +.prose th, .prose td { + text-align: left; + padding: 0.55rem 0.85rem; + border-bottom: 1px solid var(--line); + vertical-align: top; +} +.prose th { + font-family: var(--mono); + font-size: 0.72rem; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--accent); + border-bottom-color: var(--accent-dim); + white-space: nowrap; +} +.prose td { color: #C6CAD3; } +.prose tbody tr:hover td { background: var(--surface); } + +.prose img { max-width: 100%; height: auto; } + +.top nav a.on { color: var(--fg); } + +.readerlink { margin-top: 2rem; }