GitHub Code Show Whitespace

A userscript that shows whitespace (space, tabs and carriage returns) in code blocks

当前为 2017-10-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 1.0.0
  4. // @description A userscript that shows whitespace (space, tabs and carriage returns) in code blocks
  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. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=189706
  13. // @icon https://github.com/fluidicon.png
  14. // ==/UserScript==
  15. (() => {
  16. "use strict";
  17.  
  18. // include em-space & en-space?
  19. const whitespace = {
  20. "%20" : "<span class='pl-space ghcw-whitespace'> </span>",
  21. "%A0" : "<span class='pl-nbsp ghcw-whitespace'>&nbsp;</span>",
  22. "%09" : "<span class='pl-tab ghcw-whitespace'>\x09</span>",
  23. // non-matching key; applied manually
  24. "CRLF" : "<span class='pl-crlf ghcw-whitespace'></span>\n"
  25. },
  26. span = document.createElement("span"),
  27. // ignore +/- in diff code blocks
  28. regexWS = /(\x20|&nbsp;|\x09)/g,
  29. regexCR = /\r*\n$/,
  30. regexTabSize = /\btab-size-\d\b/g,
  31.  
  32. toggleButton = document.createElement("div");
  33. toggleButton.className = "ghcw-toggle btn btn-sm tooltipped tooltipped-n";
  34. toggleButton.setAttribute("aria-label", "Toggle Whitespace");
  35. toggleButton.innerHTML = "<span class='pl-tab'></span>";
  36.  
  37. GM_addStyle(`
  38. .highlight .blob-code-inner { tab-size: 2; }
  39. /* GitHub-Dark overrides the above setting */
  40. .highlight.ghcw-active.tab-size-2 .pl-tab { width: 1.1em; }
  41. .highlight.ghcw-active.tab-size-4 .pl-tab { width: 2.2em; }
  42. .highlight.ghcw-active.tab-size-6 .pl-tab { width: 3.3em; }
  43. .highlight.ghcw-active.tab-size-8 .pl-tab { width: 4.4em; }
  44.  
  45. .ghcw-active .ghcw-whitespace,
  46. .gist-content-wrapper .file-actions .btn-group {
  47. position: relative;
  48. display: inline-block;
  49. }
  50. .ghcw-active .ghcw-whitespace:before {
  51. position: absolute;
  52. opacity: .5;
  53. user-select: none;
  54. font-weight: bold;
  55. color: #777 !important;
  56. }
  57. .ghcw-toggle .pl-tab {
  58. pointer-events: none;
  59. }
  60. .ghcw-active .pl-space:before {
  61. content: "\\b7";
  62. }
  63. .ghcw-active .pl-nbsp:before {
  64. content: "\\b7";
  65. }
  66. .ghcw-active .pl-tab:before,
  67. .ghcw-toggle .pl-tab:before {
  68. content: "\\bb";
  69. }
  70. .ghcw-active .pl-crlf:before {
  71. content: "\\231d";
  72. top: -.75em;
  73. }
  74. `);
  75.  
  76. function addToggle() {
  77. $$(".file-actions").forEach(el => {
  78. if (!$(".ghcw-toggle", el)) {
  79. el.insertBefore(toggleButton.cloneNode(true), el.childNodes[0]);
  80. }
  81. });
  82. }
  83.  
  84. function getNodes(line) {
  85. const nodeIterator = document.createNodeIterator(
  86. line,
  87. NodeFilter.SHOW_TEXT,
  88. node => NodeFilter.FILTER_ACCEPT
  89. );
  90. let currentNode,
  91. nodes = [];
  92. while ((currentNode = nodeIterator.nextNode())) {
  93. nodes.push(currentNode);
  94. }
  95. return nodes;
  96. }
  97.  
  98. function escapeHTML(html) {
  99. return html.replace(/[<>"'&]/g, m => ({
  100. "<": "&lt;",
  101. ">": "&gt;",
  102. "&": "&amp;",
  103. "'": "&#39;",
  104. "\"": "&quot;"
  105. }[m]));
  106. }
  107.  
  108. function replaceWhitespace(html) {
  109. return escapeHTML(html).replace(regexWS, s => {
  110. let idx = 0,
  111. ln = s.length,
  112. result = "";
  113. for (idx = 0; idx < ln; idx++) {
  114. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  115. }
  116. return result;
  117. });
  118. }
  119.  
  120. function replaceTextNode(nodes) {
  121. let node, indx, el,
  122. ln = nodes.length;
  123. for (indx = 0; indx < ln; indx++) {
  124. node = nodes[indx];
  125. if (
  126. node &&
  127. node.nodeType === 3 &&
  128. node.textContent &&
  129. node.textContent.search(regexWS) > -1
  130. ) {
  131. el = span.cloneNode();
  132. el.innerHTML = replaceWhitespace(node.textContent.replace(regexCR, ""));
  133. node.parentNode.insertBefore(el, node);
  134. node.parentNode.removeChild(node);
  135. }
  136. }
  137. }
  138.  
  139. function addWhitespace(block) {
  140. let lines, indx, len;
  141. if (block && !block.classList.contains("ghcw-processed")) {
  142. block.classList.add("ghcw-processed");
  143. updateTabSize(block);
  144. indx = 0;
  145.  
  146. // class name of each code row
  147. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  148. len = lines.length;
  149.  
  150. // loop with delay to allow user interaction
  151. const loop = () => {
  152. let line, nodes,
  153. // max number of DOM insertions per loop
  154. max = 0;
  155. while (max < 50 && indx < len) {
  156. if (indx >= len) {
  157. return;
  158. }
  159. line = lines[indx];
  160. // first node is a syntax string and may have leading whitespace
  161. nodes = getNodes(line);
  162. replaceTextNode(nodes);
  163. // remove end CRLF if it exists; then add a line ending
  164. line.innerHTML = line.innerHTML.replace(regexCR, "") + whitespace.CRLF;
  165. max++;
  166. indx++;
  167. }
  168. if (indx < len) {
  169. setTimeout(() => {
  170. loop();
  171. }, 100);
  172. }
  173. };
  174. loop();
  175. }
  176. }
  177.  
  178. function updateTabSize(block) {
  179. // remove previous tab-size setting
  180. block.className = block.className.replace(regexTabSize, " ");
  181. // calculate tab-size; GitHub-Dark allows user modification
  182. const len = window.getComputedStyle($(".blob-code-inner", block)).tabSize;
  183. block.classList.add(`tab-size-${len}`);
  184. }
  185.  
  186. function $(selector, el) {
  187. return (el || document).querySelector(selector);
  188. }
  189.  
  190. function $$(selector, el) {
  191. return [...(el || document).querySelectorAll(selector)];
  192. }
  193.  
  194. function closest(selector, el) {
  195. while (el && el.nodeType === 1) {
  196. if (el.matches(selector)) {
  197. return el;
  198. }
  199. el = el.parentNode;
  200. }
  201. return null;
  202. }
  203.  
  204. // bind whitespace toggle button
  205. document.addEventListener("click", event => {
  206. const target = event.target;
  207. if (
  208. target.nodeName === "DIV" &&
  209. target.classList.contains("ghcw-toggle")
  210. ) {
  211. let block = $(".highlight", closest(".file", target));
  212. if (block) {
  213. target.classList.toggle("selected");
  214. block.classList.toggle("ghcw-active");
  215. updateTabSize(block);
  216. addWhitespace(block);
  217. }
  218. }
  219. });
  220.  
  221. document.addEventListener("ghmo:container", addToggle);
  222. document.addEventListener("ghmo:diff", addToggle);
  223. // toggle added to diff & file view
  224. addToggle();
  225.  
  226. })();