1234567const KINDS = [8 [/\b(release|version|bump|tag)\b/i, "accent", 1],9 [/\b(fix|bug|patch|issue|crash|error|regress|revert)\b/i, "accent-dim", 0.8],10 [/\b(add|new|implement|feat|introduce|wire|support)\b/i, "accent", 1],11 [12 /\b(refactor|restructure|reorganize|extract|move|rename|split|cleanup|clean up)\b/i,13 "text",14 0.85,15 ],16 [/\b(perf|optim|speed|fast|slow|cache|memory|alloc)\b/i, "accent", 0.9],17 [/\b(test|spec|coverage|assert)\b/i, "text-secondary", 0.74],18 [/\b(doc|readme|comment|example|guide)\b/i, "text-secondary", 0.7],19 [/\b(polish|style|format|lint|whitespace|typo|wording|copy)\b/i, "text-secondary", 0.66],20 [/\b(chore|deps|update|upgrade|build|ci|config)\b/i, "text-secondary", 0.62],21];22const DAY = 86400;2324252627function timeline() {28 const svg = document.querySelector("svg.timeline");29 if (!svg) return;30 const css = getComputedStyle(document.documentElement);31 const saves = [...svg.querySelectorAll(".dot")]32 .map((dot) => {33 const [id, message, meta] = (dot.querySelector("title")?.textContent ?? "").split("\n");34 const [, color, alpha] = KINDS.find(([kind]) => kind.test(message)) ?? [35 null,36 "text-secondary",37 0.8,38 ];39 const fill = css.getPropertyValue(`--${color}`).trim();40 return { href: dot.getAttribute("href"), id, message, meta, fill, alpha, t: +dot.dataset.t };41 })42 .sort((a, b) => a.t - b.t);43 if (saves.length === 0) return;44 const canvas = document.createElement("canvas");45 canvas.className = "timeline-canvas";46 canvas.setAttribute("role", "img");47 canvas.setAttribute("aria-label", svg.getAttribute("aria-label"));48 svg.replaceWith(canvas);49 const ctx = canvas.getContext("2d");50 const tip = document.createElement("div");51 tip.className = "timeline-tip";52 tip.hidden = true;53 document.body.append(tip);54 const reduce = matchMedia("(prefers-reduced-motion: reduce)");55 const [first, last] = [saves[0].t, saves[saves.length - 1].t];56 const end = Math.max(Date.now() / 1000, last) + DAY;57 const start = Math.min(first, Date.now() / 1000) - Math.max(0, 7 * DAY - (end - first));58 let [width, height, strip, radius, offset] = [0, 0, 0, 0, 0];59 const xOf = (t) => (strip * (t - start)) / (end - start);60 const resize = () => {61 width = canvas.parentElement.getBoundingClientRect().width;62 63 height = 64;64 canvas.width = Math.round(width * devicePixelRatio);65 canvas.height = Math.round(height * devicePixelRatio);66 canvas.style.width = `${width}px`;67 canvas.style.height = `${height}px`;68 ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);69 strip = Math.max(width * 3, saves.length * 48);70 radius = 4;71 let previous = Number.NEGATIVE_INFINITY;72 for (const save of saves) {73 save.x = Math.max(xOf(save.t), previous + radius + 8);74 previous = save.x;75 }76 offset = Math.max(0, previous - width * 0.82);77 };78 79 const onScreen = (x, margin) => {80 const shift = ((offset % strip) + strip) % strip;81 return [-1, 0, 1]82 .map((k) => x - shift + k * strip)83 .filter((sx) => sx > -margin && sx < width + margin);84 };85 const hit = (event) => {86 const box = canvas.getBoundingClientRect();87 const [mx, my] = [event.clientX - box.left, event.clientY - box.top];88 if (Math.abs(my - height / 2) > radius + 7) return null;89 return (90 saves.find((save) => onScreen(save.x, 0).some((x) => Math.abs(mx - x) < radius + 7)) ?? null91 );92 };93 let [hovered, drag, dragFrom, downX, lastX, velocity, resumeAt, lastFrame] = [94 null,95 null,96 0,97 0,98 0,99 0,100 0,101 0,102 ];103 const hover = (save, event) => {104 if (save !== hovered) {105 hovered = save;106 tip.hidden = save === null;107 if (save)108 tip.replaceChildren(109 line("tip-id", save.id),110 line("tip-msg", save.message),111 line("tip-meta", save.meta),112 );113 }114 if (save) {115 const left = event.clientX + 14;116 tip.style.left = `${left + 340 > innerWidth ? Math.max(8, event.clientX - 354) : left}px`;117 tip.style.top = `${event.clientY - 10}px`;118 }119 canvas.style.cursor = save ? "pointer" : drag === null ? "grab" : "grabbing";120 };121 const draw = (time) => {122 const dt = lastFrame ? Math.min((time - lastFrame) / 1000, 0.1) : 0;123 lastFrame = time;124 if (drag === null && hovered === null) {125 if (Math.abs(velocity) > 0.5) {126 offset -= velocity;127 velocity *= 0.92;128 } else if (!reduce.matches && time >= resumeAt) offset += 20 * dt;129 }130 const y = height / 2;131 ctx.clearRect(0, 0, width, height);132 ctx.lineWidth = 1;133 ctx.strokeStyle = css.getPropertyValue("--border");134 const years = [new Date(start * 1000).getFullYear(), new Date(end * 1000).getFullYear()];135 for (let year = years[0]; year <= years[1]; year++) {136 for (const x of onScreen(xOf(new Date(year, 0, 1) / 1000), 0)) {137 ctx.beginPath();138 ctx.moveTo(x, height * 0.1);139 ctx.lineTo(x, height * 0.9);140 ctx.stroke();141 }142 }143 ctx.globalAlpha = 0.7;144 ctx.strokeStyle = css.getPropertyValue("--text-muted");145 ctx.setLineDash([4, 4]);146 ctx.beginPath();147 ctx.moveTo(0, y);148 ctx.lineTo(width, y);149 ctx.stroke();150 ctx.setLineDash([]);151 ctx.font = '10px "Martian Mono", monospace';152 ctx.textAlign = "center";153 for (const save of saves) {154 for (const x of onScreen(save.x, 10)) {155 ctx.globalAlpha = hovered === null ? save.alpha : hovered === save ? 1 : save.alpha * 0.6;156 ctx.fillStyle = save.fill;157 ctx.beginPath();158 ctx.arc(x, y, radius, 0, Math.PI * 2);159 ctx.fill();160 if (hovered === save) {161 ctx.fillStyle = css.getPropertyValue("--accent");162 ctx.fillText(save.id.slice(0, 6), x, y - radius - 6);163 }164 }165 }166 ctx.globalAlpha = 1;167 requestAnimationFrame(draw);168 };169 canvas.addEventListener("pointerdown", (event) => {170 [drag, dragFrom, downX, lastX, velocity] = [171 event.clientX,172 offset,173 event.clientX,174 event.clientX,175 0,176 ];177 canvas.setPointerCapture(event.pointerId);178 });179 canvas.addEventListener("pointermove", (event) => {180 if (drag !== null) {181 offset = dragFrom - (event.clientX - drag);182 velocity = velocity * 0.6 + (event.clientX - lastX) * 0.4;183 lastX = event.clientX;184 }185 if (event.pointerType === "mouse") hover(hit(event), event);186 });187 const release = () => {188 drag = null;189 resumeAt = performance.now() + 900;190 };191 canvas.addEventListener("pointerup", (event) => {192 release();193 const save = Math.abs(event.clientX - downX) > 4 ? null : hit(event);194 if (save) location.href = save.href;195 });196 canvas.addEventListener("pointercancel", release);197 canvas.addEventListener("pointerleave", (event) => {198 if (drag === null) hover(null, event);199 });200 addEventListener("resize", resize);201 resize();202 requestAnimationFrame(draw);203}204205function line(className, text) {206 const div = document.createElement("div");207 div.className = className;208 div.textContent = text ?? "";209 return div;210}211212function copyButtons(root) {213 for (const button of root.querySelectorAll("button[data-copy]")) {214 const label = button.textContent;215 button.hidden = false;216 button.addEventListener("click", async () => {217 await navigator.clipboard.writeText(button.dataset.copy);218 button.textContent = "copied";219 button.classList.add("copied");220 setTimeout(() => {221 button.textContent = label;222 button.classList.remove("copied");223 }, 1400);224 });225 }226}227228229230231async function fetchMain(url, init) {232 try {233 const response = await fetch(url, init);234 const page = new DOMParser().parseFromString(await response.text(), "text/html");235 const main = page.querySelector("main");236 for (const link of main?.querySelectorAll("a[href]:not([href^='#'])") ?? []) {237 link.href = new URL(link.getAttribute("href"), response.url || url).href;238 }239 return main;240 } catch {241 return null;242 }243}244245246function modified(event) {247 return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;248}249250251252253254function panels() {255 const host = document.querySelector(".repo-panel");256 if (!host) return;257 let open = null;258 let shown = "";259 const load = async (url, init) => {260 shown = url;261 const main = await fetchMain(url, init);262 if (shown !== url) return;263 if (!main) {264 location.href = url;265 return;266 }267 main.querySelector(".breadcrumb")?.remove();268 main.querySelector(".file-page-title")?.remove();269 host.replaceChildren(...main.childNodes);270 copyButtons(host);271 host.hidden = false;272 };273 const inside = (url) => {274 const base = new URL(open.href).pathname;275 const { pathname } = new URL(url);276 return (pathname === base || pathname.startsWith(`${base}/`)) && !/\.[^/]*$/.test(pathname);277 };278 for (const tab of document.querySelectorAll(".repo-tab[data-panel]")) {279 tab.addEventListener("click", (event) => {280 if (modified(event)) return;281 event.preventDefault();282 if (open === tab && shown !== tab.href) {283 load(tab.href);284 return;285 }286 open?.classList.remove("active");287 if (open === tab) {288 open = null;289 shown = "";290 host.hidden = true;291 return;292 }293 open = tab;294 tab.classList.add("active");295 load(tab.href);296 });297 }298 host.addEventListener("click", (event) => {299 const link = event.target.closest("a[href]");300 if (!link || !open || modified(event) || link.hasAttribute("download")) return;301 if (!inside(link.href)) return;302 event.preventDefault();303 load(link.href);304 });305 host.addEventListener("submit", (event) => {306 const form = event.target;307 if (!open || !inside(form.action)) return;308 event.preventDefault();309 const method = form.method.toUpperCase();310 const data = new FormData(form);311 if (method === "GET") load(`${form.action}?${new URLSearchParams(data)}`);312 else load(form.action, { method, body: data });313 });314}315316317318function saveLists() {319 for (const list of document.querySelectorAll(".save-list")) {320 const observer = new IntersectionObserver(321 async ([entry]) => {322 const older = list.querySelector(".pager-older");323 if (!entry?.isIntersecting || !older) return;324 observer.disconnect();325 const main = await fetchMain(older.href);326 const next = main?.querySelector(`.save-list[data-list="${list.dataset.list}"]`);327 if (!next) return;328 next.querySelector(".pager a:not(.pager-older)")?.remove();329 list.querySelector(".pager").replaceWith(...next.childNodes);330 watch();331 },332 { root: list },333 );334 const watch = () => {335 const older = list.querySelector(".pager-older");336 if (older) observer.observe(older);337 };338 watch();339 }340}341342343function lineRanges() {344 const code = document.querySelector(".code");345 if (!code) return;346 let anchor = null;347 const mark = () => {348 for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl");349 const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);350 if (!match) return;351 const from = Number(match[1]);352 const to = Number(match[2] ?? match[1]);353 anchor = from;354 for (let n = Math.min(from, to); n <= Math.max(from, to); n++) {355 document.getElementById(`L${n}`)?.classList.add("hl");356 }357 document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" });358 };359 code.addEventListener("click", (event) => {360 const link = event.target.closest(".ln");361 if (!link || !event.shiftKey || anchor === null) return;362 event.preventDefault();363 const n = Number(link.textContent);364 history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`);365 mark();366 });367 window.addEventListener("hashchange", mark);368 mark();369}370371372373function atlas() {374 const root = document.querySelector(".atlas");375 const filter = document.querySelector(".atlas-filter");376 if (!root || !filter) return;377 const panel = root.querySelector(".atlas-panel");378 const folders = [...root.querySelectorAll(".atlas-folder")];379 const files = [...root.querySelectorAll(".atlas-file")];380 381 root.style.height = `${root.offsetHeight}px`;382 root.style.overflowY = "hidden";383 const key = `kepr:atlas:${root.dataset.repo}`;384 const opened = new Set(remembered(key));385 386 const unfold = new URLSearchParams(location.search).get("open");387 const onPath = (path) => unfold !== null && `${unfold}/`.startsWith(`${path}/`);388 for (const folder of folders) {389 folder.open = opened.has(folder.dataset.path) || onPath(folder.dataset.path);390 folder.addEventListener("toggle", () => {391 if (filter.value.trim()) return;392 if (folder.open) opened.add(folder.dataset.path);393 else opened.delete(folder.dataset.path);394 remember(key, [...opened]);395 });396 }397 let selected = null;398 const show = (row) => {399 selected?.classList.remove("selected");400 selected = row;401 row.classList.add("selected");402 panel.replaceChildren(row.nextElementSibling.content.cloneNode(true));403 };404 for (const row of root.querySelectorAll(".atlas-row")) {405 row.addEventListener("pointerenter", (event) => {406 if (event.pointerType === "mouse") show(row);407 });408 row.addEventListener("focus", () => show(row));409 }410 const viewer = fileViewer(root);411 for (const file of files) {412 file.addEventListener("click", (event) => {413 if (modified(event)) return;414 event.preventDefault();415 viewer(file);416 });417 }418 const target = folders.find((folder) => folder.dataset.path === unfold)?.firstElementChild;419 if (target) {420 show(target);421 target.scrollIntoView({ block: "center" });422 }423 filter.addEventListener("input", () => {424 const query = filter.value.trim().toLowerCase();425 for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query);426 for (const folder of folders) {427 folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])");428 folder.open = query !== "" || opened.has(folder.dataset.path);429 }430 });431 432 const search = document.querySelector(".atlas-search-btn");433 search.addEventListener("click", () => {434 search.hidden = true;435 filter.hidden = false;436 filter.focus();437 });438 filter.addEventListener("blur", () => {439 if (filter.value.trim() !== "") return;440 filter.hidden = true;441 search.hidden = false;442 });443 search.hidden = false;444 panel.hidden = false;445}446447448function atlasPicker() {449 const picker = document.querySelector(".atlas-picker");450 if (!picker) return;451 const filter = picker.querySelector(".atlas-picker-filter");452 const rows = [...picker.querySelectorAll(".atlas-picker-row")];453 filter.addEventListener("input", () => {454 const query = filter.value.trim().toLowerCase();455 for (const row of rows) row.hidden = query !== "" && !row.dataset.q.includes(query);456 });457 picker.addEventListener("toggle", () => {458 if (picker.open) filter.focus();459 });460 document.addEventListener("click", (event) => {461 if (!picker.contains(event.target)) picker.open = false;462 });463 picker.addEventListener("keydown", (event) => {464 if (event.key === "Escape") picker.open = false;465 });466 filter.hidden = false;467}468469470471472function fileViewer(root) {473 let [row, size, wrap] = [null, 12, false];474 const overlay = element("div", "fv-overlay");475 overlay.tabIndex = -1;476 overlay.setAttribute("role", "dialog");477 overlay.setAttribute("aria-modal", "true");478 const close = () => {479 overlay.remove();480 row?.focus();481 row = null;482 };483 const step = (by) => {484 const rows = [...root.querySelectorAll(".atlas-file")].filter((file) => !file.hidden);485 const next = rows[rows.indexOf(row) + by];486 if (next) open(next);487 };488 const restyle = () => {489 overlay.style.setProperty("--fv-size", `${size}px`);490 overlay.classList.toggle("fv-wrap", wrap);491 };492 const open = async (file, url = file.href) => {493 row = file;494 const rows = [...root.querySelectorAll(".atlas-file")].filter((each) => !each.hidden);495 const index = rows.indexOf(file);496 const detail = file.nextElementSibling.content.cloneNode(true);497 const name = detail.querySelector(".atlas-info-name").textContent;498 const path = detail.querySelector(".atlas-info-path").textContent;499 const raw = new URL(file.href);500 raw.pathname = raw.pathname.replace(/^(\/[^/]+)\/files\//, "$1/raw/");501 const button = (text, action, className = "fv-ctrl-btn") => {502 const b = element("button", className, text);503 b.type = "button";504 b.addEventListener("click", action);505 return b;506 };507 const link = (text, download) => {508 const a = element("a", "fv-ctrl-btn", text);509 a.href = raw.href;510 if (download) a.download = name;511 return a;512 };513 const source = element("div", "fv-source", "loading…");514 const panel = element("aside", "fv-detail");515 panel.append(detail);516 const header = element("div", "fv-header");517 const title = element("div", "fv-title");518 title.append(element("span", "fv-name", name), element("span", "fv-path mono", path));519 const controls = element("div", "fv-controls mono");520 controls.append(521 button("A−", () => {522 size = Math.max(9, size - 1);523 restyle();524 }),525 button("A+", () => {526 size = Math.min(20, size + 1);527 restyle();528 }),529 button("⇌ wrap", (event) => {530 wrap = !wrap;531 event.currentTarget.classList.toggle("active", wrap);532 restyle();533 }),534 link("raw", false),535 link("download", true),536 );537 controls.children[2].classList.toggle("active", wrap);538 const nav = element("div", "fv-nav mono");539 const back = button("←", () => step(-1), "fv-ctrl-btn");540 const forward = button("→", () => step(1), "fv-ctrl-btn");541 back.disabled = index <= 0;542 forward.disabled = index >= rows.length - 1;543 nav.append(back, element("span", "fv-pos", `${index + 1}/${rows.length}`), forward);544 header.append(button("×", close, "fv-close"), title, controls, nav);545 const body = element("div", "fv-body");546 body.append(source, panel);547 overlay.replaceChildren(header, body);548 restyle();549 if (!overlay.isConnected) document.body.append(overlay);550 overlay.focus();551 const main = await fetchMain(url);552 if (row !== file) return;553 if (!main) {554 location.href = url;555 return;556 }557 for (const heading of main.querySelectorAll(".breadcrumb, .file-page-title")) heading.remove();558 main.querySelector(".file-meta:not(:last-child)")?.remove();559 source.replaceChildren(...main.childNodes);560 };561 overlay.addEventListener("keydown", (event) => {562 if (event.key === "Escape") close();563 else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {564 event.preventDefault();565 step(event.key === "ArrowLeft" ? -1 : 1);566 }567 });568 569 overlay.addEventListener("click", (event) => {570 const link = event.target.closest(".fv-source a[href]");571 if (!link || !row || modified(event)) return;572 if (new URL(link.href).pathname !== new URL(row.href).pathname) return;573 event.preventDefault();574 open(row, link.href);575 });576 return open;577}578579function element(tag, className, text) {580 const node = document.createElement(tag);581 node.className = className;582 if (text !== undefined) node.textContent = text;583 return node;584}585586587588function remembered(key) {589 try {590 const value = JSON.parse(localStorage.getItem(key) ?? "[]");591 return Array.isArray(value) && value.every((path) => typeof path === "string") ? value : [];592 } catch {593 return [];594 }595}596597function remember(key, value) {598 try {599 localStorage.setItem(key, JSON.stringify(value));600 } catch {}601}602603604605function hexGlow() {606 const columns = document.querySelectorAll(".hex-col");607 if (columns.length === 0) return;608 const reduce = matchMedia("(prefers-reduced-motion: reduce)");609 let timer = null;610 const update = () => {611 clearInterval(timer);612 for (const column of columns) column.classList.remove("glow");613 if (reduce.matches) return;614 timer = setInterval(() => {615 const column = columns[Math.floor(Math.random() * columns.length)];616 if (document.querySelectorAll(".hex-col.glow").length >= 3) return;617 column.classList.add("glow");618 setTimeout(() => column.classList.remove("glow"), 3200);619 }, 1500);620 };621 reduce.addEventListener("change", update);622 update();623}624625626627function tickerPace() {628 for (const track of document.querySelectorAll(".ticker-track")) {629 const speed = Number(getComputedStyle(track).getPropertyValue("--speed"));630 if (speed > 0) track.style.animationDuration = `${track.scrollWidth / 2 / speed}s`;631 }632}633634635document.fonts.ready.then(tickerPace);636timeline();637copyButtons(document);638panels();639saveLists();640atlasPicker();641lineRanges();642atlas();643hexGlow();