GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2020-03-28 提交的版本,查看 最新版本

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