GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2021-04-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Folding
  3. // @version 1.1.3
  4. // @description A userscript that adds code folding to GitHub files
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @run-at document-idle
  11. // @grant GM.addStyle
  12. // @grant GM_addStyle
  13. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
  14. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=882023
  15. // @require https://greasyfork.org/scripts/398877-utils-js/code/utilsjs.js?version=895926
  16. // @icon https://github.githubassets.com/pinned-octocat.svg
  17. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  18. // ==/UserScript==
  19. /* global $ $$ on */
  20. /**
  21. * This userscript has been heavily modified from the "github-code-folding"
  22. * Chrome extension Copyright 2016 by Noam Lustiger; under an MIT license
  23. * https://github.com/noam3127/github-code-folding
  24. */
  25. (() => {
  26. "use strict";
  27.  
  28. GM.addStyle(`
  29. td.blob-code.blob-code-inner { position:relative; padding-left:10px; }
  30. .ghcf-collapser { position:absolute; left:2px; width:10px; cursor:pointer; }
  31. .ghcf-collapser:after { display: inline-block; vertical-align: middle;
  32. content:"\u25bc"; opacity:.5; transition:.15s; }
  33. .ghcf-collapser:hover:after { opacity:1; }
  34. .ghcf-collapsed.ghcf-collapser:after { transform:rotate(-90deg);
  35. opacity:.8; }
  36. .ghcf-hidden-line { display:none; }
  37. .ghcf-ellipsis { padding:1px 2px; margin-left:2px; cursor:pointer;
  38. background:rgba(255,235,59,.4); position:relative; z-index:1; }
  39. .ghcf-ellipsis:hover { background:rgba(255,235,59,.7); }
  40. `);
  41.  
  42. const blocks = {};
  43. const ellipsis = document.createElement("span");
  44. const triangle = document.createElement("span");
  45.  
  46. triangle.className = "ghcf-collapser";
  47. ellipsis.className = "pl-smi ghcf-ellipsis";
  48. ellipsis.innerHTML = "…";
  49.  
  50. function countInitialWhiteSpace(arr) {
  51. const getWhiteSpaceIndex = i => {
  52. if (arr[i] !== " " && arr[i] !== "\t" && arr[i] !== "\xa0") {
  53. return i;
  54. }
  55. return getWhiteSpaceIndex(++i);
  56. };
  57. return getWhiteSpaceIndex(0);
  58. }
  59.  
  60. function getPreviousSpaces(map, lineNum) {
  61. let prev = map.get(lineNum - 1);
  62. return prev === -1
  63. ? getPreviousSpaces(map, lineNum - 1)
  64. : {
  65. lineNum: lineNum - 1,
  66. count: prev
  67. };
  68. }
  69.  
  70. function getLineNumber(el) {
  71. let elm = el.closest("tr");
  72. if (elm) {
  73. elm = elm.querySelector("[data-line-number]");
  74. return elm ? parseInt(elm.dataset.lineNumber, 10) : "";
  75. }
  76. return "";
  77. }
  78.  
  79. function getCodeLines(codeBlock) {
  80. return $$(".blob-code-inner", codeBlock);
  81. }
  82.  
  83. function toggleCode({ action, codeBlock, index, depth }) {
  84. let els, lineNums;
  85. const codeLines = getCodeLines(codeBlock) || [];
  86. const pairs = blocks[codeBlock.dataset.blockIndex];
  87. if (!pairs || codeLines.length === 0) {
  88. return;
  89. }
  90. // depth is a string containing a specific depth number to toggle
  91. if (depth) {
  92. els = $$(`.ghcf-collapser[data-depth="${depth}"]`, codeBlock);
  93. lineNums = els.map(el => {
  94. el.classList.toggle("ghcf-collapsed", action === "hide");
  95. return getLineNumber(el);
  96. });
  97. } else {
  98. lineNums = [index];
  99. }
  100.  
  101. if (action === "hide") {
  102. lineNums.forEach(start => {
  103. let elm;
  104. let end = pairs.get(start - 1);
  105. codeLines.slice(start, end).forEach(el => {
  106. elm = el.closest("tr");
  107. if (elm) {
  108. elm.classList.add("ghcf-hidden-line");
  109. }
  110. });
  111. if (!$(".ghcf-ellipsis", codeLines[start - 1])) {
  112. elm = $(".ghcf-collapser", codeLines[start - 1]);
  113. elm.parentNode.insertBefore(
  114. ellipsis.cloneNode(true),
  115. null
  116. );
  117. }
  118. });
  119. } else if (action === "show") {
  120. lineNums.forEach(start => {
  121. let end = pairs.get(start - 1);
  122. codeLines.slice(start, end).forEach(el => {
  123. let elm = el.closest("tr");
  124. if (elm) {
  125. elm.classList.remove("ghcf-hidden-line");
  126. removeEls(".ghcf-ellipsis", elm);
  127. }
  128. elm = $(".ghcf-collapsed", elm);
  129. if (elm) {
  130. elm.classList.remove("ghcf-collapsed");
  131. }
  132. });
  133. removeEls(".ghcf-ellipsis", codeLines[start - 1]);
  134. });
  135. }
  136. // shift ends up selecting text on the page, so clear it
  137. if (lineNums.length > 1) {
  138. removeSelection();
  139. }
  140. }
  141.  
  142. function addBindings() {
  143. on(document, "click", event => {
  144. let index, elm, isCollapsed;
  145. const el = event.target;
  146. const codeBlock = el.closest(".highlight");
  147.  
  148. // click on collapser
  149. if (el && el.classList.contains("ghcf-collapser")) {
  150. isCollapsed = el.classList.contains("ghcf-collapsed");
  151. index = getLineNumber(el);
  152. // Shift + click to toggle them all
  153. if (index && event.getModifierState("Shift")) {
  154. return toggleCode({
  155. action: isCollapsed ? "show" : "hide",
  156. codeBlock,
  157. index,
  158. depth: el.dataset.depth
  159. });
  160. }
  161. if (index) {
  162. if (isCollapsed) {
  163. el.classList.remove("ghcf-collapsed");
  164. toggleCode({ action: "show", codeBlock, index });
  165. } else {
  166. el.classList.add("ghcf-collapsed");
  167. toggleCode({ action: "hide", codeBlock, index });
  168. }
  169. }
  170. return;
  171. }
  172.  
  173. // click on ellipsis
  174. if (el && el.classList.contains("ghcf-ellipsis")) {
  175. elm = $(".ghcf-collapsed", el.parentNode);
  176. if (elm) {
  177. elm.classList.remove("ghcf-collapsed");
  178. }
  179. index = getLineNumber(el);
  180. if (index) {
  181. toggleCode({ action: "show", codeBlock, index });
  182. }
  183. }
  184. });
  185. }
  186.  
  187. function addCodeFolding() {
  188. // Keep .file in case someone needs this userscript for GitHub Enterprise
  189. if ($(".file table.highlight, .blob-wrapper table.highlight")) {
  190. $$("table.highlight").forEach((codeBlock, blockIndex) => {
  191. if (codeBlock && codeBlock.classList.contains("ghcf-processed")) {
  192. // Already processed
  193. return;
  194. }
  195. const codeLines = getCodeLines(codeBlock);
  196. removeEls("span.ghcf-collapser", codeBlock);
  197. if (codeLines) {
  198. // In case this script has already been run and modified the DOM on a
  199. // previous page in github, make sure to reset it.
  200. codeBlock.classList.add("ghcf-processed");
  201. codeBlock.dataset.blockIndex = blockIndex;
  202.  
  203. const spaceMap = new Map();
  204. const stack = [];
  205. const pairs = blocks[blockIndex] = new Map();
  206.  
  207. codeLines.forEach((el, lineNum) => {
  208. let prevSpaces;
  209. let line = el.textContent;
  210. let count = line.trim().length
  211. ? countInitialWhiteSpace(line.split(""))
  212. : -1;
  213. spaceMap.set(lineNum, count);
  214.  
  215. function tryPair() {
  216. let el;
  217. let top = stack[stack.length - 1];
  218. if (count !== -1 && count <= spaceMap.get(top)) {
  219. pairs.set(top, lineNum);
  220. // prepend triangle
  221. el = triangle.cloneNode();
  222. el.dataset.depth = count + 1;
  223. codeLines[top].insertBefore(el, codeLines[top].childNodes[0]);
  224. stack.pop();
  225. return tryPair();
  226. }
  227. }
  228. tryPair();
  229.  
  230. prevSpaces = getPreviousSpaces(spaceMap, lineNum);
  231. if (count > prevSpaces.count) {
  232. stack.push(prevSpaces.lineNum);
  233. }
  234. });
  235. }
  236. });
  237. }
  238. }
  239.  
  240. function removeEls(selector, el) {
  241. let els = $$(selector, el);
  242. let index = els.length;
  243. while (index--) {
  244. els[index].parentNode.removeChild(els[index]);
  245. }
  246. }
  247.  
  248. function removeSelection() {
  249. // remove text selection - https://stackoverflow.com/a/3171348/145346
  250. const sel = window.getSelection
  251. ? window.getSelection()
  252. : document.selection;
  253. if (sel) {
  254. if (sel.removeAllRanges) {
  255. sel.removeAllRanges();
  256. } else if (sel.empty) {
  257. sel.empty();
  258. }
  259. }
  260. }
  261.  
  262. on(document, "ghmo:container", addCodeFolding);
  263. addCodeFolding();
  264. addBindings();
  265.  
  266. })();