GitHub Code Folding

A userscript that adds code folding to GitHub files

当前为 2020-05-12 提交的版本,查看 最新版本

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