GitHub Code Folding

A userscript that adds code folding to GitHub files

目前為 2017-10-11 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Folding
  3. // @version 1.0.8
  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. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=198500
  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:10px; }
  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); position:relative; z-index:1; }
  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 elm,
  89. end = pairs.get(start - 1);
  90. codeLines.slice(start, end).forEach(el => {
  91. elm = closest("tr", el);
  92. if (elm) {
  93. elm.classList.add("hidden-line");
  94. }
  95. });
  96. if (!$(".ellipsis", codeLines[start - 1])) {
  97. elm = $(".collapser", codeLines[start - 1]);
  98. elm.parentNode.insertBefore(
  99. ellipsis.cloneNode(true),
  100. elm.nextSibling
  101. );
  102. }
  103. });
  104. } else if (action === "show") {
  105. lineNums.forEach(start => {
  106. let end = pairs.get(start - 1);
  107. codeLines.slice(start, end).forEach(el => {
  108. let elm = closest("tr", el);
  109. if (elm) {
  110. elm.classList.remove("hidden-line");
  111. remove(".ellipsis", elm);
  112. }
  113. elm = $(".sideways", elm);
  114. if (elm) {
  115. elm.classList.remove("sideways");
  116. }
  117. });
  118. remove(".ellipsis", codeLines[start - 1]);
  119. });
  120. }
  121. // shift ends up selecting text on the page, so clear it
  122. if (lineNums.length > 1) {
  123. removeSelection();
  124. }
  125. }
  126.  
  127. function addBindings() {
  128. document.addEventListener("click", event => {
  129. let index, elm, isCollapsed;
  130. const el = event.target;
  131.  
  132. // click on collapser
  133. if (el && el.classList.contains("collapser")) {
  134. isCollapsed = el.classList.contains("sideways");
  135. index = getLineNumber(el);
  136. // Shift + click to toggle them all
  137. if (index && event.getModifierState("Shift")) {
  138. return toggleCode(
  139. isCollapsed ? "show" : "hide",
  140. index,
  141. el.getAttribute("data-depth")
  142. );
  143. }
  144. if (index) {
  145. if (isCollapsed) {
  146. el.classList.remove("sideways");
  147. toggleCode("show", index);
  148. } else {
  149. el.classList.add("sideways");
  150. toggleCode("hide", index);
  151. }
  152. }
  153. return;
  154. }
  155.  
  156. // click on ellipsis
  157. if (el && el.classList.contains("ellipsis")) {
  158. elm = $(".sideways", el.parentNode);
  159. if (elm) {
  160. elm.classList.remove("sideways");
  161. }
  162. index = getLineNumber(el);
  163. if (index) {
  164. toggleCode("show", index);
  165. }
  166. }
  167. });
  168. }
  169.  
  170. function addCodeFolding() {
  171. if ($(".file table.highlight")) {
  172. // In case this script has already been run and modified the DOM on a
  173. // previous page in github, make sure to reset it.
  174. remove("span.collapser");
  175. pairs.clear();
  176.  
  177. const codeLines = $$(".file table.highlight .blob-code-inner"),
  178. spaceMap = new Map(),
  179. stack = [];
  180.  
  181. codeLines.forEach((el, lineNum) => {
  182. let prevSpaces,
  183. line = el.textContent,
  184. count = line.trim().length ?
  185. countInitialWhiteSpace(line.split("")) :
  186. -1;
  187. spaceMap.set(lineNum, count);
  188.  
  189. function tryPair() {
  190. let el,
  191. top = stack[stack.length - 1];
  192. if (count !== -1 && count <= spaceMap.get(top)) {
  193. pairs.set(top, lineNum);
  194. // prepend triangle
  195. el = triangle.cloneNode();
  196. el.setAttribute("data-depth", count + 1);
  197. codeLines[top].appendChild(el, codeLines[top].childNodes[0]);
  198. stack.pop();
  199. return tryPair();
  200. }
  201. }
  202. tryPair();
  203.  
  204. prevSpaces = getPreviousSpaces(spaceMap, lineNum);
  205. if (count > prevSpaces.count) {
  206. stack.push(prevSpaces.lineNum);
  207. }
  208. });
  209. }
  210. }
  211.  
  212. function $(selector, el) {
  213. return (el || document).querySelector(selector);
  214. }
  215.  
  216. function $$(selector, el) {
  217. return Array.from((el || document).querySelectorAll(selector));
  218. }
  219.  
  220. function closest(selector, el) {
  221. while (el && el.nodeType === 1) {
  222. if (el.matches(selector)) {
  223. return el;
  224. }
  225. el = el.parentNode;
  226. }
  227. return null;
  228. }
  229.  
  230. function remove(selector, el) {
  231. let els = $$(selector, el),
  232. index = els.length;
  233. while (index--) {
  234. els[index].parentNode.removeChild(els[index]);
  235. }
  236. }
  237.  
  238. function removeSelection() {
  239. // remove text selection - https://stackoverflow.com/a/3171348/145346
  240. const sel = window.getSelection ?
  241. window.getSelection() :
  242. document.selection;
  243. if (sel) {
  244. if (sel.removeAllRanges) {
  245. sel.removeAllRanges();
  246. } else if (sel.empty) {
  247. sel.empty();
  248. }
  249. }
  250. }
  251.  
  252. document.addEventListener("ghmo:container", addCodeFolding);
  253. addCodeFolding();
  254. addBindings();
  255.  
  256. })();