GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2019-01-29 提交的版本,查看 最新版本

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