GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2016-12-29 提交的版本,查看 最新版本

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