GitHub Code Folding

A userscript that adds code folding to GitHub files

2017-03-25 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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