// lines.js — picking lines on the file view.
//
// Every gutter number is a real link to ?ln=N#LN, so with no script a
// click still names a line. With script a click picks without a reload:
// the first click picks a line, a second makes a range from it, a third
// starts again, and shift-click extends whatever is picked. Each pick
// pushes the address, so Back steps through them.
//
// Delegated from the document, like copy.js: the next window of the
// file arrives as a patched fragment and its numbers work unchanged.
(function () {
"use strict";
function parse(s) {
var out = [];
(s || "").split(/[,+ ]/).forEach(function (part) {
var m = /^(\d+)(?:-(\d+))?$/.exec(part);
if (!m) return;
var a = +m[1], b = m[2] ? +m[2] : a;
if (a < 1 || b < 1) return;
out.push(a <= b ? [a, b] : [b, a]);
});
out.sort(function (x, y) { return x[0] - y[0]; });
var merged = [];
out.forEach(function (r) {
var last = merged[merged.length - 1];
if (last && r[0] <= last[1] + 1) last[1] = Math.max(last[1], r[1]);
else merged.push(r);
});
return merged;
}
function format(ranges) {
return ranges.map(function (r) {
return r[0] === r[1] ? String(r[0]) : r[0] + "-" + r[1];
}).join(",");
}
function label(ranges) {
var one = ranges.length === 1 && ranges[0][0] === ranges[0][1];
return (one ? "Line " : "Lines ") + ranges.map(function (r) {
return r[0] === r[1] ? String(r[0]) : r[0] + "–" + r[1];
}).join(", ");
}
function current() {
return parse(new URLSearchParams(location.search).get("ln"));
}
function paint(ranges) {
var body = document.getElementById("file-body");
if (!body) return;
body.querySelectorAll(".line.hl").forEach(function (el) { el.classList.remove("hl"); });
ranges.forEach(function (r) {
for (var n = r[0]; n <= r[1]; n++) {
var el = document.getElementById("L" + n);
if (!el) break; // past what is drawn, or past the end
el.classList.add("hl");
}
});
var bar = document.querySelector("[data-hook=line-bar]");
if (!bar) return;
bar.hidden = ranges.length === 0;
var what = bar.querySelector("[data-hook=line-range]");
if (what && ranges.length) what.textContent = label(ranges);
var past = bar.querySelector("[data-hook=line-past]");
if (past) past.hidden = true; // only the server knows the end
var link = bar.querySelector("[data-hook=line-link]");
if (link) link.href = bar.getAttribute("data-base") + "?ln=" + format(ranges);
}
function go(ranges) {
var q = ranges.length ? "?ln=" + format(ranges) : "";
history.pushState(null, "", location.pathname + q);
paint(ranges);
}
var anchor = 0; // the line a range grows from
function pick(n, extend) {
var sel = current();
if (!anchor && sel.length) anchor = sel[0][0];
if (extend && anchor) return go([[Math.min(anchor, n), Math.max(anchor, n)]]);
if (sel.length === 1 && sel[0][0] === sel[0][1] && sel[0][0] !== n) {
anchor = sel[0][0];
return go([[Math.min(anchor, n), Math.max(anchor, n)]]);
}
anchor = n;
go([[n, n]]);
}
// Copying selects the text first, which always works and is the
// native copy gesture on a phone, then tries both copy paths: the
// Clipboard API is missing on a plain-HTTP origin.
function copy(text, button) {
var area = document.createElement("textarea");
area.value = text;
area.setAttribute("readonly", "");
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
var copied = false;
try { copied = document.execCommand("copy"); } catch (e) { /* refused */ }
area.remove();
var done = function () {
button.classList.add("done");
setTimeout(function () { button.classList.remove("done"); }, 3000);
};
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(done, function () { if (copied) done(); });
} else if (copied) {
done();
}
}
document.addEventListener("click", function (ev) {
var t = ev.target.closest && ev.target.closest("#file-body a.ln, [data-hook=line-bar] a, [data-hook=line-bar] button");
if (!t || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.altKey) return;
var bar = t.closest("[data-hook=line-bar]");
if (!bar) {
ev.preventDefault();
pick(+t.getAttribute("data-n"), ev.shiftKey);
return;
}
switch (t.getAttribute("data-hook")) {
case "line-link":
ev.preventDefault();
copy(new URL(t.getAttribute("href"), location.href).href, t);
break;
case "line-copy":
var text = [];
document.querySelectorAll("#file-body .line.hl .cl").forEach(function (el) {
text.push(el.textContent.replace(/\n$/, ""));
});
copy(text.join("\n") + "\n", t);
break;
case "line-clear":
ev.preventDefault();
anchor = 0;
go([]);
break;
}
});
window.addEventListener("popstate", function () { paint(current()); });
})();