From 3afdd1a6b3895cd2be0d1099abff8188cfa89c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 17 Aug 2020 01:43:23 +0200 Subject: [PATCH] Extract line highlighting from blob into own file --- _static/linelight.js | 251 +++++++++++++++++++++++++++++++++++ gitsrht/templates/blob.html | 255 +----------------------------------- 2 files changed, 252 insertions(+), 254 deletions(-) create mode 100755 _static/linelight.js diff --git a/_static/linelight.js b/_static/linelight.js new file mode 100755 index 0000000..2dde195 --- /dev/null +++ b/_static/linelight.js @@ -0,0 +1,251 @@ +/* + * @licstart This JavaScript code is licensed under the AGPL 3.0 license. + * See https://git.sr.ht/~sircmpwn/git.sr.ht/tree/master/LICENSE for details. + * @licend + */ + +/** + * Matches URL hash selecting one or more lines: + * - #L10 - single line + * - #L10,20 - multiple lines + * - #L10-15 - span of lines + * - #L10-15,20-25 - multiple spans + * - #L10,15-20,30 - combination of above + */ +const hashPattern = /^#L(\d+(-\d+)?)(,\d+(-\d+)?)*$/; + +const isValidHash = hash => hash.match(hashPattern); + +const getLine = no => document.getElementById(`L${no}`); + +const getLineCount = () => document.querySelectorAll('.lines > a').length; + +const lineNumber = line => Number(line.id.substring(1)); + +function* range(start, end) { + if (end < start) { + [start, end] = [end, start]; + } + + for (let n = start; n <= end; n += 1) { + yield n; + } +} + +/** + * Given a string representation of a span returns the numbers contained in it. + * Numbers greater than max are ignored. + */ +const parseSpan = (span, max) => { + const [sStart, sEnd] = span.includes("-") ? span.split("-") : [span, span]; + const [start, end] = [sStart, sEnd].map(Number).sort((a, b) => a - b); + + if (start > max) { + return []; + } else if (end > max) { + return range(start, max); + } else { + return range(start, end); + } +} + +/** + * Returns a set of line numbers matching the hash. + */ +const lineNumbersFromHash = hash => { + const lineCount = getLineCount(); + const lineNos = new Set(); + + if (isValidHash(hash)) { + const spans = location.hash.substring(2).split(","); + for (let span of spans) { + for (let no of parseSpan(span, lineCount)) { + lineNos.add(no); + } + } + } + + return lineNos; +} + +/** + * Given a set of line numbers, groups them into spans. + * Yields tuples of [startNo, endNo]. + */ +const spansFromLineNumbers = lineNos => { + if (lineNos.size === 0) { + return []; + } + + const sorted = Array.from(lineNos).sort((a, b) => a - b); + const spans = []; + let current, prev; + let start = sorted[0]; + + for (current of sorted) { + if (prev && current !== prev + 1) { + spans.push([start, prev]); + start = current; + } + prev = current; + } + spans.push([start, current]); + + return spans; +} + +/** + * Returns a hash matching the given set of line numbers. + */ +const hashFromLineNumbers = lineNos => { + const spans = spansFromLineNumbers(lineNos); + const parts = []; + + for ([start, end] of spans) { + if (start == end) { + parts.push(start); + } else { + parts.push([start, end].join("-")); + } + } + + return "#L" + parts.join(","); +} + +const selectLine = lineNo => { + const line = getLine(lineNo); + if (line) { + line.classList.add("selected"); + } +} + +const selectLines = lineNos => { + for (lineNo of lineNos) { + selectLine(lineNo); + } +} + +const unselectLine = lineNo => { + const line = getLine(lineNo); + if (line) { + line.classList.remove("selected"); + } +} + +const unselectAll = () => { + const selected = document.querySelectorAll(".lines .selected"); + for (let line of selected) { + line.classList.remove("selected"); + } +} + +const handlePlainClick = (selected, lineNo) => { + selected.clear(); + selected.add(lineNo); + unselectAll(); + selectLine(lineNo); +} + +const handleCtrlClick = (selected, lineNo) => { + if (selected.has(lineNo)) { + selected.delete(lineNo); + unselectLine(lineNo); + } else { + selected.add(lineNo); + selectLine(lineNo); + } +} + +const handleShiftClick = (selected, lineNo, lastNo) => { + if (lastNo) { + for (no of range(lastNo, lineNo)) { + selected.add(no); + selectLine(no); + } + } +} + +/** + * Scroll the selected lines into view. + */ +const scrollToSelected = (selected) => { + if (selected.size > 0) { + const firstNo = Math.min(...selected); + const scrollNo = Math.max(firstNo - 5, 1); // add top padding + const line = getLine(scrollNo); + if (line) { + line.scrollIntoView(); + } + } +} + +/** + * Returns true if two sets contain the same elements. + */ +const setsEqual = (a, b) => { + if (a.size != b.size) { + return false; + } + for (n of a) { + if (!b.has(n)) { + return false; + } + } + return true; +} + +/** + * A set of currently selected line numbers. + */ +let selected = lineNumbersFromHash(location.hash); + +/** + * The number of the last line to be clicked. Used to select spans of lines. + * If a single line is selected initially, set to that line. + */ +let lastNo = selected.size == 1 ? Array.from(selected)[0] : null; + +/** + * Overrides default click handler for line numbers. + */ +const handleLineClicked = event => { + event.preventDefault(); + + const lineNo = lineNumber(event.target); + if (event.ctrlKey) { + handleCtrlClick(selected, lineNo); + } else if (event.shiftKey) { + handleShiftClick(selected, lineNo, lastNo); + } else { + handlePlainClick(selected, lineNo); + } + + lastNo = lineNo; + + const hash = hashFromLineNumbers(selected); + if (hash) { + window.location.hash = hash; + } else { + // Hacky way to clear the hash (https://stackoverflow.com/a/15323220) + history.pushState('', document.title, window.location.pathname); + } +} + +// Catch when the hash is changed from the outside and update the selection +// e.g. when the user edits the hash in the URL +window.onhashchange = () => { + let newSelected = lineNumbersFromHash(location.hash); + if (!setsEqual(selected, newSelected)) { + selected = newSelected; + unselectAll(); + selectLines(selected); + } +} + +document.querySelectorAll('.lines a').forEach( + line => line.addEventListener("click", handleLineClicked) +); + +// Initially select lines matching hash and scroll them into view +selectLines(selected); +scrollToSelected(selected); diff --git a/gitsrht/templates/blob.html b/gitsrht/templates/blob.html index bc92644..51519a3 100644 --- a/gitsrht/templates/blob.html +++ b/gitsrht/templates/blob.html @@ -133,258 +133,5 @@ pre, body { {% endblock %} {% block scripts %} - - + {% endblock %} -- 2.38.4