GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2017-04-24 提交的版本,查看 最新版本

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