A _static/linelight.js => _static/linelight.js +251 -0
@@ 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);
M gitsrht/templates/blob.html => gitsrht/templates/blob.html +1 -254
@@ 133,258 133,5 @@ pre, body {
{% endblock %}
{% block scripts %}
-<script>
-/*
- * @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
- */
-</script>
-<script>
-/**
- * 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);
-</script>
+<script src="/static/linelight.js"></script>
{% endblock %}