Home: - The hero stands on a light from the upper left and a pool of the accent, mixed from Niveus tokens so it follows the theme. The mark sits in an .nv-tile key under a slogan: "Every layer its own -- from the first instruction to the last pixel." - New section headings: "A teaching kernel that outgrew its lessons", "Measured, not marketed", "One ISO. One stick. Your disks untouched." - A Wallpapers section with the four new ones, each downloadable as a 4K JPEG or as SVG. The four-card grids are two across, so none is left alone on its row. Docs reader: - An index filter, a breadcrumb with reading time and source path, code blocks framed with a language strip and a Copy key, previous and next keys at the foot, a reading-progress hairline, and the contents entry lit for the section on screen. - The index lists the documents it was missing: scheduler, FAT, audio, LDLS, debugging and the hardware report. - On a phone the page is no longer wider than the screen (a pre block was sizing the grid's auto column), and the index folds away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
514 lines
20 KiB
JavaScript
514 lines
20 KiB
JavaScript
/* 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, "<").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++;
|
|
/* A frame rather than a bare block: a title strip naming the language,
|
|
and a button that copies it, because the thing a reader most often
|
|
wants from a code block in these docs is to paste it somewhere. */
|
|
out.push('<figure class="codeframe">' +
|
|
'<figcaption class="codeframe__bar">' +
|
|
'<span class="codeframe__lang">' + esc(lang || "text") + "</span>" +
|
|
'<button class="nv-button nv-button--ghost nv-button--sm" type="button" data-copy>Copy</button>' +
|
|
"</figcaption>" +
|
|
'<pre class="nv-code doc__pre"' +
|
|
(lang ? ' data-lang="' + esc(lang) + '"' : "") +
|
|
"><code>" + esc(buf.join("\n")) + "</code></pre></figure>");
|
|
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="nv-table-frame"><table class="nv-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 = "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 '<header class="dochead">' +
|
|
'<ol class="nv-breadcrumbs">' +
|
|
'<li><a href="?doc=README.md">Docs</a></li>' +
|
|
"<li>" + esc(at.group) + "</li>" +
|
|
'<li aria-current="page">' + esc(at.title) + "</li>" +
|
|
"</ol>" +
|
|
'<span class="dochead__meta nv-caption nv-text-tertiary">' +
|
|
mins + " min read · " +
|
|
'<a href="../' + esc(path) + '">' + esc(path) + "</a>" +
|
|
"</span>" +
|
|
"</header>";
|
|
}
|
|
|
|
/* The next and previous document, as two keys at the foot of the page. A
|
|
reader who reaches the end of one has nearly always come to read the
|
|
section, not the file. */
|
|
function pager(path) {
|
|
var k = indexOf(path), html = '<nav class="docpager" aria-label="More documents">';
|
|
function key(d, dir) {
|
|
return '<a class="nv-card nv-card--interactive docpager__key docpager__key--' + dir +
|
|
'" href="' + docHref(d.path) + '">' +
|
|
'<span class="nv-overline nv-text-tertiary">' + (dir === "prev" ? "← Previous" : "Next →") + "</span>" +
|
|
'<span class="nv-title">' + esc(d.title) + "</span>" +
|
|
'<span class="nv-caption nv-text-tertiary">' + esc(d.group) + "</span></a>";
|
|
}
|
|
if (k > 0) html += key(FLAT[k - 1], "prev");
|
|
else html += "<span></span>";
|
|
if (k >= 0 && k < FLAT.length - 1) html += key(FLAT[k + 1], "next");
|
|
return html + "</nav>";
|
|
}
|
|
|
|
/* Light the contents entry for the section being read. */
|
|
var spy;
|
|
function watchHeadings() {
|
|
if (spy) spy.disconnect();
|
|
if (!("IntersectionObserver" in window)) return;
|
|
var links = {};
|
|
Array.prototype.forEach.call(tocEl.querySelectorAll("a"), function (a) {
|
|
links[a.getAttribute("href").slice(1)] = a;
|
|
});
|
|
spy = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (e) {
|
|
if (!e.isIntersecting || !links[e.target.id]) return;
|
|
Array.prototype.forEach.call(tocEl.querySelectorAll("a"), function (a) {
|
|
a.removeAttribute("aria-current");
|
|
});
|
|
links[e.target.id].setAttribute("aria-current", "location");
|
|
});
|
|
}, { rootMargin: "0px 0px -70% 0px" });
|
|
Array.prototype.forEach.call(article.querySelectorAll("h2[id], h3[id]"), function (h) {
|
|
spy.observe(h);
|
|
});
|
|
}
|
|
|
|
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 = header(path, text) + render(text, path, headings) + pager(path);
|
|
buildToc(headings);
|
|
watchHeadings();
|
|
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="nv-code doc__pre"><code>cd path/to/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());
|
|
|
|
/* Fold the index on a narrow screen, and fold it again after picking a
|
|
document there -- the choice is made, and the document is what was asked
|
|
for. */
|
|
var fold = document.getElementById("docindex");
|
|
var narrow = window.matchMedia && window.matchMedia("(max-width: 64rem)");
|
|
if (fold && narrow && narrow.matches) fold.open = false;
|
|
nav.addEventListener("click", function () {
|
|
if (fold && narrow && narrow.matches) fold.open = false;
|
|
});
|
|
|
|
/* Filter the index as you type. Groups with nothing left in them go too,
|
|
so the heading of an empty section does not sit there implying a match. */
|
|
var filter = document.getElementById("docfilter");
|
|
if (filter) filter.addEventListener("input", function () {
|
|
var q = filter.value.trim().toLowerCase();
|
|
Array.prototype.forEach.call(nav.querySelectorAll(".nv-nav__list"), function (ul) {
|
|
var any = false;
|
|
Array.prototype.forEach.call(ul.querySelectorAll("li"), function (li) {
|
|
var hit = !q || li.textContent.toLowerCase().indexOf(q) >= 0;
|
|
li.hidden = !hit;
|
|
any = any || hit;
|
|
});
|
|
ul.hidden = !any;
|
|
if (ul.previousElementSibling) ul.previousElementSibling.hidden = !any;
|
|
});
|
|
});
|
|
|
|
/* Copy a code block. The label says what happened, then goes back. */
|
|
article.addEventListener("click", function (ev) {
|
|
var b = ev.target.closest ? ev.target.closest("[data-copy]") : null;
|
|
if (!b) return;
|
|
var code = b.closest(".codeframe").querySelector("code").textContent;
|
|
var done = function (ok) {
|
|
b.textContent = ok ? "Copied" : "Select and copy";
|
|
setTimeout(function () { b.textContent = "Copy"; }, 1600);
|
|
};
|
|
if (navigator.clipboard) navigator.clipboard.writeText(code).then(function () { done(true); }, function () { done(false); });
|
|
else done(false);
|
|
});
|
|
|
|
/* How far down the document the reader is: a hairline under the bar. */
|
|
var bar = document.getElementById("readbar");
|
|
if (bar) window.addEventListener("scroll", function () {
|
|
var h = document.documentElement;
|
|
var f = h.scrollHeight > h.clientHeight ? h.scrollTop / (h.scrollHeight - h.clientHeight) : 0;
|
|
bar.style.transform = "scaleX(" + Math.min(1, Math.max(0, f)) + ")";
|
|
}, { passive: true });
|
|
|
|
/* 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()); });
|
|
})();
|