GitHub Diff Expander

A userscript that adds more diff code expanding buttons

当前为 2022-10-24 提交的版本,查看 最新版本

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