GitHub Code Folding

A userscript that adds code folding to GitHub files

As of 2017-12-15. See the latest version.

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