Greasy Fork is available in English.

GitHub Code Folding

A userscript that adds code folding to GitHub files

2018-05-28 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name GitHub Code Folding
  3. // @version 1.0.14
  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?updated=20180103
  13. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=597950
  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 { position:relative; 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. let isMobile = false;
  41.  
  42. triangle.className = "collapser";
  43. ellipsis.className = "pl-smi ellipsis";
  44. ellipsis.innerHTML = "…";
  45.  
  46. function countInitialWhiteSpace(arr) {
  47. const getWhiteSpaceIndex = i => {
  48. if (arr[i] !== " " && arr[i] !== "\t" &&
  49. (!isMobile || arr[i] !== "\xa0")) {
  50. return i;
  51. }
  52. i++;
  53. return getWhiteSpaceIndex(i);
  54. };
  55. return getWhiteSpaceIndex(0);
  56. }
  57.  
  58. function getPreviousSpaces(map, lineNum) {
  59. let prev = map.get(lineNum - 1);
  60. return prev === -1 ?
  61. getPreviousSpaces(map, lineNum - 1) : {
  62. lineNum: lineNum - 1,
  63. count: prev
  64. };
  65. }
  66.  
  67. function getLineNumber(el) {
  68. let elm = el.closest(isMobile ? "div" : "td"),
  69. index = elm ? elm.id : "";
  70. if (index) {
  71. return parseInt(index.slice(2), 10);
  72. }
  73. return "";
  74. }
  75.  
  76. function getCodeLines() {
  77. return $$(isMobile ? ".blob-file-content .line" :
  78. ".file table.highlight .blob-code-inner");
  79. }
  80.  
  81. function toggleCode(action, index, depth) {
  82. let els, lineNums;
  83. const codeLines = getCodeLines();
  84. // depth is a string containing a specific depth number to toggle
  85. if (depth) {
  86. els = $$(`.collapser[data-depth="${depth}"]`);
  87. lineNums = els.map(el => {
  88. el.classList.toggle("sideways", action === "hide");
  89. return getLineNumber(el);
  90. });
  91. } else {
  92. lineNums = [index];
  93. }
  94.  
  95. if (action === "hide") {
  96. lineNums.forEach(start => {
  97. let elm,
  98. end = pairs.get(start - 1);
  99. codeLines.slice(start, end).forEach(el => {
  100. elm = isMobile ? el : el.closest("tr");
  101. if (elm) {
  102. elm.classList.add("hidden-line");
  103. }
  104. });
  105. if (!$(".ellipsis", codeLines[start - 1])) {
  106. elm = $(".collapser", codeLines[start - 1]);
  107. elm.parentNode.insertBefore(
  108. ellipsis.cloneNode(true),
  109. elm.nextSibling
  110. );
  111. }
  112. });
  113. } else if (action === "show") {
  114. lineNums.forEach(start => {
  115. let end = pairs.get(start - 1);
  116. codeLines.slice(start, end).forEach(el => {
  117. let elm = isMobile ? el : el.closest("tr");
  118. if (elm) {
  119. elm.classList.remove("hidden-line");
  120. remove(".ellipsis", elm);
  121. }
  122. elm = $(".sideways", elm);
  123. if (elm) {
  124. elm.classList.remove("sideways");
  125. }
  126. });
  127. remove(".ellipsis", codeLines[start - 1]);
  128. });
  129. }
  130. // shift ends up selecting text on the page, so clear it
  131. if (lineNums.length > 1) {
  132. removeSelection();
  133. }
  134. }
  135.  
  136. function addBindings() {
  137. document.addEventListener("click", event => {
  138. let index, elm, isCollapsed;
  139. const el = event.target;
  140.  
  141. // click on collapser
  142. if (el && el.classList.contains("collapser")) {
  143. isCollapsed = el.classList.contains("sideways");
  144. index = getLineNumber(el);
  145. // Shift + click to toggle them all
  146. if (index && event.getModifierState("Shift")) {
  147. return toggleCode(
  148. isCollapsed ? "show" : "hide",
  149. index,
  150. el.getAttribute("data-depth")
  151. );
  152. }
  153. if (index) {
  154. if (isCollapsed) {
  155. el.classList.remove("sideways");
  156. toggleCode("show", index);
  157. } else {
  158. el.classList.add("sideways");
  159. toggleCode("hide", index);
  160. }
  161. }
  162. return;
  163. }
  164.  
  165. // click on ellipsis
  166. if (el && el.classList.contains("ellipsis")) {
  167. elm = $(".sideways", el.parentNode);
  168. if (elm) {
  169. elm.classList.remove("sideways");
  170. }
  171. index = getLineNumber(el);
  172. if (index) {
  173. toggleCode("show", index);
  174. }
  175. }
  176. });
  177. }
  178.  
  179. function addCodeFolding() {
  180. if ($(".file table.highlight") || $("div.blob-file-content")) {
  181. isMobile = !$(".file table.highlight");
  182.  
  183. // In case this script has already been run and modified the DOM on a
  184. // previous page in github, make sure to reset it.
  185. remove("span.collapser");
  186. pairs.clear();
  187.  
  188. const codeLines = getCodeLines(),
  189. spaceMap = new Map(),
  190. stack = [];
  191.  
  192. codeLines.forEach((el, lineNum) => {
  193. let prevSpaces,
  194. line = el.textContent,
  195. count = line.trim().length ?
  196. countInitialWhiteSpace(line.split("")) :
  197. -1;
  198. spaceMap.set(lineNum, count);
  199.  
  200. function tryPair() {
  201. let el,
  202. top = stack[stack.length - 1];
  203. if (count !== -1 && count <= spaceMap.get(top)) {
  204. pairs.set(top, lineNum);
  205. // prepend triangle
  206. el = triangle.cloneNode();
  207. el.setAttribute("data-depth", count + 1);
  208. codeLines[top].appendChild(el, codeLines[top].childNodes[0]);
  209. stack.pop();
  210. return tryPair();
  211. }
  212. }
  213. tryPair();
  214.  
  215. prevSpaces = getPreviousSpaces(spaceMap, lineNum);
  216. if (count > prevSpaces.count) {
  217. stack.push(prevSpaces.lineNum);
  218. }
  219. });
  220. }
  221. }
  222.  
  223. function $(selector, el) {
  224. return (el || document).querySelector(selector);
  225. }
  226.  
  227. function $$(selector, el) {
  228. return Array.from((el || document).querySelectorAll(selector));
  229. }
  230.  
  231. function remove(selector, el) {
  232. let els = $$(selector, el),
  233. index = els.length;
  234. while (index--) {
  235. els[index].parentNode.removeChild(els[index]);
  236. }
  237. }
  238.  
  239. function removeSelection() {
  240. // remove text selection - https://stackoverflow.com/a/3171348/145346
  241. const sel = window.getSelection ?
  242. window.getSelection() :
  243. document.selection;
  244. if (sel) {
  245. if (sel.removeAllRanges) {
  246. sel.removeAllRanges();
  247. } else if (sel.empty) {
  248. sel.empty();
  249. }
  250. }
  251. }
  252.  
  253. document.addEventListener("ghmo:container", addCodeFolding);
  254. addCodeFolding();
  255. addBindings();
  256.  
  257. })();