size: 3 KiB
| 1 | const millisecondsPerSecond = 1000; |
| 2 | const secondsPerMinute = 60; |
| 3 | const minutesPerHour = 60; |
| 4 | const hoursPerDay = 24; |
| 5 | const daysPerWeek = 7; |
| 6 | const daysPerMonth = 31; |
| 7 | const daysPerYear = 365; // getting imprecise here, but no big deal |
| 8 | const 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 | }; |
| 32 | const relativeDateFormat = new Intl.RelativeTimeFormat("en", { style: "long" }); |
| 33 | |
| 34 | // https://stackoverflow.com/a/78704662 |
| 35 | function 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 | |
| 48 | document.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 | }); |