GitHub Diff Expander

A userscript that adds more diff code expanding buttons

  1. // ==UserScript==
  2. // @name GitHub Diff Expander
  3. // @version 0.1.5
  4. // @description A userscript that adds more diff code expanding buttons
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @match https://github.com/*
  9. // @run-at document-idle
  10. // @grant none
  11. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
  12. // @icon https://github.githubassets.com/pinned-octocat.svg
  13. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  14. // ==/UserScript==
  15.  
  16. (() => {
  17. "use strict";
  18.  
  19. // A max of 20 (0-19) rows are loaded; set by GitHub server
  20. const expandMax = 20;
  21. // The expand server call expects these parameters; but they may have no
  22. // setting (first expander), or be completely missing (last expander)
  23. const order = [
  24. "prev_line_num_left",
  25. "next_line_num_left",
  26. "prev_line_num_right",
  27. "next_line_num_right"
  28. ];
  29. const regexp = order.reduce((acc, line) => {
  30. acc[line] = new RegExp(`${line}=\\d*`);
  31. return acc;
  32. }, {});
  33.  
  34. const span = document.createElement("span");
  35. let busy = false;
  36.  
  37. function parse(num) {
  38. return Number(num) || 0;
  39. }
  40.  
  41. function getRange(tr, isExpander) {
  42. let range = [];
  43. if (isExpander) {
  44. const link = $("a.diff-expander", tr);
  45. // Using data-(left/right)-range because the data-url may not include all
  46. // the values
  47. [range[0], range[1]] = link.dataset.leftRange.split("-").map(parse);
  48. [range[2], range[3]] = link.dataset.rightRange.split("-").map(parse);
  49. // but dataset range[0] is equal to prev_line_num_left after expanding.
  50. // To get the correct number, the start needs to be increased by two
  51. const prev = tr.previousElementSibling;
  52. if (prev && prev.classList.contains("blob-expanded")) {
  53. range[0] = range[0] + 2;
  54. range[2] = range[2] + 2;
  55. }
  56. } else {
  57. range[0] = range[1] = parse($("td:first-child", tr).dataset.lineNumber);
  58. range[2] = range[3] = parse($("td:nth-child(2)", tr).dataset.lineNumber);
  59. }
  60. return range;
  61. }
  62.  
  63. function removeExpanders(file) {
  64. $$("tr.ghdex-expander", file).forEach(tr => {
  65. tr.parentNode.removeChild(tr);
  66. });
  67. }
  68.  
  69. function findGaps(file) {
  70. let left = 0;
  71. let right = 0;
  72. removeExpanders(file);
  73. // It's not efficient, but cycling through every row was found to be the
  74. // most reliable method of getting an accurate expander range
  75. $$("tr", file).forEach(row => {
  76. const isExpander = row.classList.contains("js-expandable-line");
  77. const range = getRange(row, isExpander);
  78. if (isExpander) {
  79. let max = left + expandMax;
  80. if (typeof row.dataset.position === "undefined") {
  81. // Last expander row data-url parameter doesn't include either
  82. // "next_line_num_(left/right)"
  83. return updateExpander(row, [], [
  84. range[0],
  85. Math.min(range[1], range[0] + expandMax - 1)
  86. ]);
  87. } else if (max > range[1] && range[1] - left < expandMax) {
  88. range[0] = Math.max(1, left, range[0]);
  89. range[2] = Math.max(1, right, range[2]);
  90. } else if (range[1] > max && left < range[1] - expandMax) {
  91. addExpander(row, [left, max, right, right + expandMax]);
  92. } else if (range[1] - expandMax > left) {
  93. range[0] = left;
  94. range[2] = right;
  95. }
  96. const text = [
  97. Math.max(1, range[0], range[1] - expandMax + 1),
  98. range[1],
  99. Math.max(1, range[2], range[3] - expandMax + 1),
  100. range[3]
  101. ];
  102. updateExpander(row, range, text);
  103. } else {
  104. left = range[0];
  105. right = range[2];
  106. }
  107. });
  108. }
  109.  
  110. function updateExpander(tr, range, text = range) {
  111. const expander = $("a.diff-expander", tr);
  112. if (range.length) {
  113. expander.dataset.leftRange = range.slice(0, 2).join("-");
  114. expander.dataset.rightRange = range.slice(2).join("-");
  115. let result = expander.dataset.url;
  116. range.forEach((value, index) => {
  117. // prev_line_num_(left/right) requires an empty value to get line 1 (0)
  118. // next_line_num_(left/right) requires the *next* line number + 1, so a
  119. // range of 1-20 would have [0, 21, 0, 21] and a range of 20-40 would be
  120. // [20, 41, 20, 41]
  121. result = replacer(result, order[index], value + index % 2);
  122. });
  123. expander.dataset.url = result;
  124. }
  125. // The last expander won't include text[2] & text[3] values
  126. const [start, end] = text;
  127. const txt = start > end ? `${end}-${end}` : `${start}-${end}`;
  128. let el = $("span", expander);
  129. if (el) {
  130. el.textContent = txt;
  131. } else {
  132. span.textContent = txt;
  133. expander.insertAdjacentElement("beforeend", span.cloneNode(true));
  134. }
  135. }
  136.  
  137. function replacer(url, order, value) {
  138. // The first expander will require a key with no value to load the first
  139. // line, i.e. "...&prev_line_num_left=&prev_line_num_right=&..."
  140. const result = `${order}=${value <= 1 ? "" : value}`;
  141. const regex = regexp[order];
  142. // The last expander row doesn't include next_line_num_(left/right);
  143. // it'll need to be added
  144. return regex.test(url) ? url.replace(regex, result) : url + result;
  145. }
  146.  
  147. function addExpander(tr, range) {
  148. const expander = tr.cloneNode(true);
  149. expander.classList.add("ghdex-expander");
  150. $("td.blob-code", expander).textContent = "\xA0";
  151. expander.removeAttribute("data-position");
  152. tr.parentNode.insertBefore(expander, tr);
  153. const text = [range[0] + 1, range[1], range[2] + 1, range[3]];
  154. updateExpander(expander, range, text);
  155. }
  156.  
  157. function processFiles() {
  158. busy = true;
  159. $$(".diff-table").forEach(findGaps);
  160. setTimeout(() => {
  161. busy = false;
  162. }, 500);
  163. }
  164.  
  165. function init(e) {
  166. if (!busy && $("#diff-0")) {
  167. if (e) {
  168. setTimeout(processFiles, 100);
  169. } else {
  170. processFiles();
  171. }
  172. }
  173. }
  174.  
  175. function $(selector, el) {
  176. return (el || document).querySelector(selector);
  177. }
  178.  
  179. function $$(selectors, el) {
  180. return [...(el || document).querySelectorAll(selectors)];
  181. }
  182.  
  183. document.addEventListener("ghmo:container", init);
  184. document.addEventListener("ghmo:diff", init);
  185. init();
  186.  
  187. })();