- // ==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();
-
- })();