size: 3 KiB

1const millisecondsPerSecond = 1000;
2const secondsPerMinute = 60;
3const minutesPerHour = 60;
4const hoursPerDay = 24;
5const daysPerWeek = 7;
6const daysPerMonth = 31;
7const daysPerYear = 365; // getting imprecise here, but no big deal
8const intervals = {
9 year:
10 millisecondsPerSecond *
11 secondsPerMinute *
12 minutesPerHour *
13 hoursPerDay *
14 daysPerYear,
15 month:
16 millisecondsPerSecond *
17 secondsPerMinute *
18 minutesPerHour *
19 hoursPerDay *
20 daysPerMonth,
21 week:
22 millisecondsPerSecond *
23 secondsPerMinute *
24 minutesPerHour *
25 hoursPerDay *
26 daysPerWeek,
27 day: millisecondsPerSecond * secondsPerMinute * minutesPerHour * hoursPerDay,
28 hour: millisecondsPerSecond * secondsPerMinute * minutesPerHour,
29 minute: millisecondsPerSecond * secondsPerMinute,
30 second: millisecondsPerSecond,
31};
32const relativeDateFormat = new Intl.RelativeTimeFormat("en", { style: "long" });
33
34// https://stackoverflow.com/a/78704662
35function formatRelativeTime(isoStr) {
36 const diff = new Date(isoStr) - new Date();
37 for (const interval in intervals) {
38 if (intervals[interval] <= Math.abs(diff)) {
39 return relativeDateFormat.format(
40 Math.trunc(diff / intervals[interval]),
41 interval,
42 );
43 }
44 }
45 return relativeDateFormat.format(diff / 1000, "second");
46}
47
48document.addEventListener("DOMContentLoaded", function () {
49 // Progressive enhancement: If javascript is enabled, certain <time> elements
50 // will be rewritten from ISO datetime strings to human-readable relative text
51 // (e.g. 2 days ago).
52 document.querySelectorAll("time.relative").forEach((element) => {
53 const timeStr = element.getAttribute("datetime");
54 if (!timeStr) return;
55 const relativeTimeStr = formatRelativeTime(timeStr);
56 element.innerHTML = relativeTimeStr;
57 });
58
59 // Shift-click to select a line range
60 const SELECTED_RANGE = "selected-range";
61 document.querySelectorAll(".line-number > a").forEach((el) => {
62 el.addEventListener("click", (evt) => {
63 if (evt.shiftKey && location.hash.match("^#L[0-9]+$")) {
64 evt.preventDefault();
65 document.querySelectorAll("." + SELECTED_RANGE).forEach((el) => {
66 el.classList.remove(SELECTED_RANGE);
67 });
68
69 const line1 = new Number(location.hash.slice(2));
70 const line2 = new Number(evt.target.innerHTML);
71 const [startLineNum, endLineNum] = [line1, line2].sort((a, b) => a - b);
72
73 for (let i = startLineNum; i <= endLineNum; i++) {
74 const selector = `a[href="#L${i}"]`;
75 const el = document.querySelector(selector);
76 el.parentElement.parentElement.classList.add(SELECTED_RANGE);
77 }
78
79 document.location.hash = `#L${startLineNum}-${endLineNum}`;
80 } else {
81 document.querySelectorAll("." + SELECTED_RANGE).forEach((el) => {
82 el.classList.remove(SELECTED_RANGE);
83 });
84 }
85 });
86 });
87
88 // On page load, focus on and scroll to selected range
89 const selectedRange = location.hash.match("^#L([0-9]+)-([0-9]+)$");
90 if (selectedRange) {
91 const startLineNum = new Number(selectedRange[1]);
92 const endLineNum = new Number(selectedRange[2]);
93 for (let i = startLineNum; i <= endLineNum; i++) {
94 const selector = `a[href="#L${i}"]`;
95 const el = document.querySelector(selector);
96 el.parentElement.parentElement.classList.add(SELECTED_RANGE);
97 }
98 document.querySelector(`#L${startLineNum}`).scrollIntoView();
99 }
100});