Files
Rayhaan JaufeerallyandClaude Opus 5.5 dc275f99d7
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
Add a documentation website, built with Zola
The documentation moves from READMEs into a site in docs/: getting
started, a user guide, operations, a reference, and development pages,
written in the style of network vendors' documentation (docs/STYLE.md).
bin/bgp/README.md and docs/RFCs.md are folded into it.

- A theme of its own in the International Typographic Style, in blues,
  with self-hosted Inter and JetBrains Mono, search over Zola's index,
  dark mode, and no resources loaded from other hosts.
- Example configurations in docs/examples/, shown on the pages and
  checked with `bgp --check` in CI.
- A test that fails when a field of config.proto is missing from the
  configuration reference.
- A CI job that builds the site; Zola fails on broken links and anchors.
- A hardened systemd unit, tried under systemd with kernel routes on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:12:43 +02:00

243 lines
9.2 KiB
JavaScript

// The little the site does in the browser: the theme switch, the contents
// on narrow screens, copying code, following the reader in "On this page",
// and search over the index Zola builds. Nothing is fetched from elsewhere.
"use strict";
(function () {
var root = document.documentElement;
// --- Theme ---------------------------------------------------------------
function currentTheme() {
if (root.dataset.theme) return root.dataset.theme;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function remember(theme) {
try { localStorage.setItem("theme", theme); } catch (e) {}
}
document.querySelectorAll("[data-theme-toggle]").forEach(function (button) {
button.addEventListener("click", function () {
var next = currentTheme() === "dark" ? "light" : "dark";
root.dataset.theme = next;
remember(next);
});
});
// --- Contents, on narrow screens -----------------------------------------
var menuButton = document.querySelector("[data-menu-toggle]");
function setMenu(open) {
document.body.classList.toggle("menu-open", open);
if (menuButton) menuButton.setAttribute("aria-expanded", String(open));
}
if (menuButton) {
menuButton.addEventListener("click", function () {
setMenu(!document.body.classList.contains("menu-open"));
});
document.addEventListener("click", function (event) {
if (!document.body.classList.contains("menu-open")) return;
if (event.target.closest("[data-sidebar], [data-menu-toggle]")) return;
setMenu(false);
});
}
// --- Code blocks: a button to copy ----------------------------------------
var COPY = '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2"/><path d="M15.5 8.5V5.5a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3"/></svg>';
var DONE = '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>';
document.querySelectorAll(".prose pre").forEach(function (pre) {
var wrap = document.createElement("div");
wrap.className = "code-block";
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
var code = pre.querySelector("code");
var button = document.createElement("button");
button.type = "button";
button.className = "copy-button";
button.setAttribute("aria-label", "Copy");
button.innerHTML = COPY;
button.addEventListener("click", function () {
var text = (code || pre).innerText.replace(/\n$/, "");
navigator.clipboard.writeText(text).then(function () {
button.innerHTML = DONE;
button.classList.add("copied");
setTimeout(function () {
button.innerHTML = COPY;
button.classList.remove("copied");
}, 1400);
});
});
wrap.appendChild(button);
});
// --- On this page: the heading being read --------------------------------
var tocLinks = Array.prototype.slice.call(document.querySelectorAll("[data-toc-link]"));
if (tocLinks.length && "IntersectionObserver" in window) {
var byId = {};
var headings = [];
tocLinks.forEach(function (link) {
var id = decodeURIComponent(link.hash.slice(1));
var heading = document.getElementById(id);
if (heading) { byId[id] = link; headings.push(heading); }
});
var visible = new Set();
function mark() {
// The first heading on screen; failing that, the last one above it.
var current = null;
for (var i = 0; i < headings.length; i++) {
if (visible.has(headings[i])) { current = headings[i]; break; }
if (headings[i].getBoundingClientRect().top < 120) current = headings[i];
}
tocLinks.forEach(function (link) { link.classList.remove("active"); });
if (current) byId[current.id].classList.add("active");
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) visible.add(entry.target); else visible.delete(entry.target);
});
mark();
}, { rootMargin: "-60px 0px -55% 0px" });
headings.forEach(function (heading) { observer.observe(heading); });
}
// --- Search --------------------------------------------------------------
var search = document.querySelector("[data-search]");
var input = document.querySelector("[data-search-input]");
var results = document.querySelector("[data-search-results]");
var empty = document.querySelector("[data-search-empty]");
var index = null;
var loading = null;
var selected = -1;
function load(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement("script");
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function loadIndex() {
if (!loading) {
loading = load(document.body.dataset.searchEngine)
.then(function () { return load(document.body.dataset.searchIndex); })
.then(function () { index = window.elasticlunr.Index.load(window.searchIndex); });
}
return loading;
}
function openSearch() {
search.hidden = false;
document.body.style.overflow = "hidden";
input.focus();
input.select();
loadIndex().then(run);
}
function closeSearch() {
search.hidden = true;
document.body.style.overflow = "";
}
function escape(text) {
return text.replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
// A piece of the page around the first word searched for.
function excerpt(body, terms) {
var lower = body.toLowerCase();
var at = -1;
for (var i = 0; i < terms.length && at < 0; i++) at = lower.indexOf(terms[i]);
var start = Math.max(0, at - 60);
var text = (start > 0 ? "…" : "") + body.slice(start, start + 220);
var html = escape(text);
terms.forEach(function (term) {
if (term.length < 2) return;
var pattern = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
html = html.replace(pattern, "<mark>$1</mark>");
});
return html;
}
function sectionOf(url) {
var path = url.replace(/^https?:\/\/[^/]+/, "");
var parts = path.split("/").filter(Boolean);
if (parts[0] !== "docs" || parts.length < 3) return "";
return parts[1].replace(/-/g, " ").replace(/^./, function (c) { return c.toUpperCase(); });
}
function run() {
if (!index) return;
var query = input.value.trim();
results.innerHTML = "";
selected = -1;
empty.hidden = true;
if (!query) return;
var found = index.search(query, {
bool: "AND",
expand: true,
fields: { title: { boost: 3 }, description: { boost: 2 }, body: { boost: 1 } },
}).slice(0, 10);
if (!found.length) { empty.hidden = false; return; }
var terms = query.toLowerCase().split(/\s+/).filter(Boolean);
found.forEach(function (hit, i) {
var doc = index.documentStore.getDoc(hit.ref);
var item = document.createElement("li");
var section = sectionOf(hit.ref);
item.innerHTML =
'<a href="' + escape(hit.ref) + '" role="option" id="search-' + i + '">' +
(section ? '<span class="search-result-section">' + escape(section) + "</span>" : "") +
'<span class="search-result-title">' + escape(doc.title || "Ferrix BGP") + "</span>" +
'<span class="search-result-text">' + excerpt(doc.body || doc.description || "", terms) + "</span></a>";
results.appendChild(item);
});
select(0);
}
function select(i) {
var links = results.querySelectorAll("a");
if (!links.length) return;
selected = (i + links.length) % links.length;
links.forEach(function (link, j) { link.setAttribute("aria-selected", String(j === selected)); });
links[selected].scrollIntoView({ block: "nearest" });
}
if (search && input) {
document.querySelectorAll("[data-search-open]").forEach(function (button) {
button.addEventListener("click", openSearch);
});
document.querySelectorAll("[data-search-close]").forEach(function (el) {
el.addEventListener("click", closeSearch);
});
input.addEventListener("input", run);
input.addEventListener("keydown", function (event) {
if (event.key === "ArrowDown") { event.preventDefault(); select(selected + 1); }
else if (event.key === "ArrowUp") { event.preventDefault(); select(selected - 1); }
else if (event.key === "Enter") {
var link = results.querySelectorAll("a")[selected];
if (link) window.location.href = link.href;
}
});
document.addEventListener("keydown", function (event) {
var typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName) || document.activeElement.isContentEditable;
if (event.key === "Escape" && !search.hidden) { closeSearch(); return; }
if (event.key === "Escape") { setMenu(false); return; }
if ((event.key === "/" && !typing) || (event.key === "k" && (event.metaKey || event.ctrlKey))) {
event.preventDefault();
openSearch();
}
});
}
})();