Download
161 lines 6.2 KiBJavaScriptModified 2026-10-20 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
// Small progressive enhancements: the light/dark toggle, the clone "Copy"
// button, fitting the tag list's frame to its contents, and dates shown as
// "2 days ago". The toggle and the button are hidden by CSS unless this
// script has marked the root, so without JavaScript the page follows the
// system colour scheme, the clone box is a plain read-only field, and dates
// stay as the page wrote them.

// timeAgo describes the time from then to now (both in milliseconds) the
// way people say it: "just now", "5 minutes ago", "yesterday", "3 weeks
// ago", "last year". A time in the future, from a skewed clock, is "just
// now".
var relativeFormat = null;
function timeAgo(then, now) {
	"use strict";
	var s = (now - then) / 1000;
	var units = [
		["year", 365.25 * 86400], ["month", 30.44 * 86400], ["week", 7 * 86400],
		["day", 86400], ["hour", 3600], ["minute", 60],
	];
	// Days give way to weeks only after a week, weeks to months only after
	// a month, and so on: each unit is used until the next one up reaches 1.
	for (var i = 0; i < units.length; i++) {
		var n = Math.floor(s / units[i][1]);
		if (n < 1) continue;
		// Built once: a listing has a date on every row.
		if (!relativeFormat) relativeFormat = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
		return relativeFormat.format(-n, units[i][0]);
	}
	return "just now";
}

if (typeof module !== "undefined" && module.exports) module.exports = { timeAgo: timeAgo };

(function () {
	"use strict";
	if (typeof document === "undefined") return;
	var root = document.documentElement;
	var KEY = "repo-render-theme";
	root.classList.add("js");

	function store(v) { try { localStorage.setItem(KEY, v); } catch (e) { /* private mode */ } }
	var saved = null;
	try { saved = localStorage.getItem(KEY); } catch (e) { /* ignore */ }
	// Applied in <head>, before first paint, so there is no flash.
	if (saved === "light" || saved === "dark") root.setAttribute("data-theme", saved);
	// A toggle elsewhere (the page around the drawer's frame, or another
	// tab) arrives as a storage event.
	window.addEventListener("storage", function (e) {
		if (e.key !== KEY) return;
		if (e.newValue === "light" || e.newValue === "dark") root.setAttribute("data-theme", e.newValue);
		else root.removeAttribute("data-theme");
	});

	function effective() {
		var t = root.getAttribute("data-theme");
		if (t) return t;
		return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
	}

	function ready() {
		var toggles = document.querySelectorAll("[data-theme-toggle]");
		for (var i = 0; i < toggles.length; i++) {
			toggles[i].addEventListener("click", function () {
				var next = effective() === "dark" ? "light" : "dark";
				root.setAttribute("data-theme", next);
				store(next);
			});
		}

		var copies = document.querySelectorAll("[data-copy]");
		Array.prototype.forEach.call(copies, function (btn) {
			var box = btn.parentNode;
			var input = box && box.querySelector("input");
			if (!input) return;
			var label = btn.textContent;
			var timer;
			btn.addEventListener("click", function () {
				var done = function (ok) {
					btn.textContent = ok ? "Copied" : "Press Ctrl+C";
					clearTimeout(timer);
					timer = setTimeout(function () { btn.textContent = label; }, 1500);
				};
				var fail = function () {
					input.focus();
					input.select();
					done(false);
				};
				// The clipboard API exists only in secure contexts: over
				// plain HTTP, select the text for the reader to copy.
				if (!navigator.clipboard) return fail();
				navigator.clipboard.writeText(input.value).then(function () { done(true); }, fail);
			});
		});

		// Without script the frame keeps its CSS height and scrolls.
		var frames = document.querySelectorAll("iframe[data-fit]");
		Array.prototype.forEach.call(frames, function (frame) {
			frame.addEventListener("load", function () {
				var body = frame.contentDocument && frame.contentDocument.body;
				if (body) frame.style.height = body.scrollHeight + "px";
			});
		});
	}

	// The download menu is a plain <details>; with script it also closes on
	// Escape or a click elsewhere.
	function closeMenus(except) {
		var open = document.querySelectorAll("details.dl[open]");
		for (var i = 0; i < open.length; i++) if (open[i] !== except) open[i].removeAttribute("open");
	}
	document.addEventListener("click", function (e) {
		closeMenus(e.target.closest("details.dl"));
	});
	document.addEventListener("keydown", function (e) {
		if (e.key !== "Escape") return;
		// Focus inside a menu would be left on a hidden link: move it
		// back to the menu's button.
		var menu = document.activeElement && document.activeElement.closest("details.dl[open]");
		closeMenus(null);
		if (menu) menu.querySelector("summary").focus();
	});

	// Dates: each <time class="ago"> carries its full timestamp, and the
	// page shows the date. Rewriting the text as the parser adds it, from
	// this script in <head>, means the date is never painted first: mutation
	// observers run before the browser next renders. The tooltip becomes
	// the reader's local date and time.
	var now = Date.now();
	function relative(t) {
		var when = Date.parse(t.getAttribute("datetime"));
		if (isNaN(when)) return;
		var text = timeAgo(when, now);
		// The parser may add the date after the element: fix it again then,
		// but leave text that is already right, or this would never settle.
		if (t.textContent !== text) t.textContent = text;
		var full = new Date(when).toLocaleString();
		if (t.title !== full) t.title = full;
	}
	function relativeIn(node) {
		if (node.nodeType !== 1) node = node.parentNode;
		if (!node || !node.closest) return;
		var t = node.closest("time.ago");
		if (t) return relative(t);
		var all = node.querySelectorAll("time.ago");
		for (var i = 0; i < all.length; i++) relative(all[i]);
	}
	var watch = new MutationObserver(function (records) {
		for (var i = 0; i < records.length; i++) {
			for (var j = 0; j < records[i].addedNodes.length; j++) relativeIn(records[i].addedNodes[j]);
		}
	});
	watch.observe(root, { childList: true, subtree: true });
	document.addEventListener("DOMContentLoaded", function () { watch.disconnect(); });

	function done() {
		relativeIn(root);
		ready();
	}
	if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", done);
	else done();
})();