// ==UserScript==
// @name GitHub Diff Expander
// @version 0.1.5
// @description A userscript that adds more diff code expanding buttons
// @license MIT
// @author Rob Garrison
// @namespace https://github.com/Mottie
// @match https://github.com/*
// @run-at document-idle
// @grant none
// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @icon https://github.githubassets.com/pinned-octocat.svg
// @supportURL https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==
(() => {
"use strict";
// A max of 20 (0-19) rows are loaded; set by GitHub server
const expandMax = 20;
// The expand server call expects these parameters; but they may have no
// setting (first expander), or be completely missing (last expander)
const order = [
"prev_line_num_left",
"next_line_num_left",
"prev_line_num_right",
"next_line_num_right"
];
const regexp = order.reduce((acc, line) => {
acc[line] = new RegExp(`${line}=\\d*`);
return acc;
}, {});
const span = document.createElement("span");
let busy = false;
function parse(num) {
return Number(num) || 0;
}
function getRange(tr, isExpander) {
let range = [];
if (isExpander) {
const link = $("a.diff-expander", tr);
// Using data-(left/right)-range because the data-url may not include all
// the values
[range[0], range[1]] = link.dataset.leftRange.split("-").map(parse);
[range[2], range[3]] = link.dataset.rightRange.split("-").map(parse);
// but dataset range[0] is equal to prev_line_num_left after expanding.
// To get the correct number, the start needs to be increased by two
const prev = tr.previousElementSibling;
if (prev && prev.classList.contains("blob-expanded")) {
range[0] = range[0] + 2;
range[2] = range[2] + 2;
}
} else {
range[0] = range[1] = parse($("td:first-child", tr).dataset.lineNumber);
range[2] = range[3] = parse($("td:nth-child(2)", tr).dataset.lineNumber);
}
return range;
}
function removeExpanders(file) {
$$("tr.ghdex-expander", file).forEach(tr => {
tr.parentNode.removeChild(tr);
});
}
function findGaps(file) {
let left = 0;
let right = 0;
removeExpanders(file);
// It's not efficient, but cycling through every row was found to be the
// most reliable method of getting an accurate expander range
$$("tr", file).forEach(row => {
const isExpander = row.classList.contains("js-expandable-line");
const range = getRange(row, isExpander);
if (isExpander) {
let max = left + expandMax;
if (typeof row.dataset.position === "undefined") {
// Last expander row data-url parameter doesn't include either
// "next_line_num_(left/right)"
return updateExpander(row, [], [
range[0],
Math.min(range[1], range[0] + expandMax - 1)
]);
} else if (max > range[1] && range[1] - left < expandMax) {
range[0] = Math.max(1, left, range[0]);
range[2] = Math.max(1, right, range[2]);
} else if (range[1] > max && left < range[1] - expandMax) {
addExpander(row, [left, max, right, right + expandMax]);
} else if (range[1] - expandMax > left) {
range[0] = left;
range[2] = right;
}
const text = [
Math.max(1, range[0], range[1] - expandMax + 1),
range[1],
Math.max(1, range[2], range[3] - expandMax + 1),
range[3]
];
updateExpander(row, range, text);
} else {
left = range[0];
right = range[2];
}
});
}
function updateExpander(tr, range, text = range) {
const expander = $("a.diff-expander", tr);
if (range.length) {
expander.dataset.leftRange = range.slice(0, 2).join("-");
expander.dataset.rightRange = range.slice(2).join("-");
let result = expander.dataset.url;
range.forEach((value, index) => {
// prev_line_num_(left/right) requires an empty value to get line 1 (0)
// next_line_num_(left/right) requires the *next* line number + 1, so a
// range of 1-20 would have [0, 21, 0, 21] and a range of 20-40 would be
// [20, 41, 20, 41]
result = replacer(result, order[index], value + index % 2);
});
expander.dataset.url = result;
}
// The last expander won't include text[2] & text[3] values
const [start, end] = text;
const txt = start > end ? `${end}-${end}` : `${start}-${end}`;
let el = $("span", expander);
if (el) {
el.textContent = txt;
} else {
span.textContent = txt;
expander.insertAdjacentElement("beforeend", span.cloneNode(true));
}
}
function replacer(url, order, value) {
// The first expander will require a key with no value to load the first
// line, i.e. "...&prev_line_num_left=&prev_line_num_right=&..."
const result = `${order}=${value <= 1 ? "" : value}`;
const regex = regexp[order];
// The last expander row doesn't include next_line_num_(left/right);
// it'll need to be added
return regex.test(url) ? url.replace(regex, result) : url + result;
}
function addExpander(tr, range) {
const expander = tr.cloneNode(true);
expander.classList.add("ghdex-expander");
$("td.blob-code", expander).textContent = "\xA0";
expander.removeAttribute("data-position");
tr.parentNode.insertBefore(expander, tr);
const text = [range[0] + 1, range[1], range[2] + 1, range[3]];
updateExpander(expander, range, text);
}
function processFiles() {
busy = true;
$$(".diff-table").forEach(findGaps);
setTimeout(() => {
busy = false;
}, 500);
}
function init(e) {
if (!busy && $("#diff-0")) {
if (e) {
setTimeout(processFiles, 100);
} else {
processFiles();
}
}
}
function $(selector, el) {
return (el || document).querySelector(selector);
}
function $$(selectors, el) {
return [...(el || document).querySelectorAll(selectors)];
}
document.addEventListener("ghmo:container", init);
document.addEventListener("ghmo:diff", init);
init();
})();