GitHub Code Show Whitespace

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

当前为 2017-09-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 0.1.8
  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)+|(\x20|&nbsp;|\x09)+$/g,
  29. regexTrailingWS = /(\x20|&nbsp;|\x09)+$/,
  30. regexCR = /\r*\n$/,
  31. regexTabSize = /\btab-size-\d\b/g,
  32.  
  33. toggleButton = document.createElement("div");
  34. toggleButton.className = "ghcw-toggle btn btn-sm tooltipped tooltipped-n";
  35. toggleButton.setAttribute("aria-label", "Toggle Whitespace");
  36. toggleButton.innerHTML = "<span class='pl-tab'></span>";
  37.  
  38. GM_addStyle(`
  39. .highlight .blob-code-inner { tab-size: 2; }
  40. /* GitHub-Dark overrides the above setting */
  41. .highlight.ghcw-active.tab-size-2 .pl-tab { width: 1.1em; }
  42. .highlight.ghcw-active.tab-size-4 .pl-tab { width: 2.2em; }
  43. .highlight.ghcw-active.tab-size-6 .pl-tab { width: 3.3em; }
  44. .highlight.ghcw-active.tab-size-8 .pl-tab { width: 4.4em; }
  45.  
  46. .ghcw-active .ghcw-whitespace,
  47. .gist-content-wrapper .file-actions .btn-group {
  48. position: relative;
  49. display: inline-block;
  50. }
  51. .ghcw-active .ghcw-whitespace:before {
  52. position: absolute;
  53. overflow: hidden;
  54. opacity: .4;
  55. user-select: none;
  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 replaceWhitespace(html) {
  85. return html
  86. .replace(regexWS, s => {
  87. let idx = 0,
  88. ln = s.length,
  89. result = "";
  90. for (idx = 0; idx < ln; idx++) {
  91. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  92. }
  93. return result;
  94. });
  95. }
  96.  
  97. function replaceTextNode(el) {
  98. let indx = el.textContent.search(regexTrailingWS);
  99. if (indx > -1) {
  100. let node = el.splitText(indx);
  101. const elm = span.cloneNode();
  102. elm.innerHTML = replaceWhitespace(
  103. node.textContent.replace(regexCR, "")
  104. );
  105. node.parentNode.insertBefore(elm, node);
  106. node.parentNode.removeChild(node);
  107. }
  108. }
  109.  
  110. function addWhitespace(block) {
  111. let lines, indx, len;
  112. if (block && !block.classList.contains("ghcw-processed")) {
  113. block.classList.add("ghcw-processed");
  114. updateTabSize(block);
  115. indx = 0;
  116.  
  117. // class name of each code row
  118. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  119. len = lines.length;
  120.  
  121. const checkNode = el => {
  122. if (el) {
  123. if (el.nodeType === 3 && el.textContent) {
  124. replaceTextNode(el);
  125. } else if (
  126. el.nodeType === 1 &&
  127. el.matches(".pl-s, .pl-s1, .pl-c") &&
  128. el.firstChild &&
  129. el.firstChild.nodeType === 3
  130. ) {
  131. el.innerHTML = replaceWhitespace(el.innerHTML);
  132. }
  133. }
  134. };
  135.  
  136. // loop with delay to allow user interaction
  137. const loop = () => {
  138. let line,
  139. // max number of DOM insertions per loop
  140. max = 0;
  141. while (max < 50 && indx < len) {
  142. if (indx >= len) {
  143. return;
  144. }
  145. line = lines[indx];
  146. // first node is a syntax string and may have leading whitespace
  147. checkNode(line.firstChild);
  148. checkNode(line.lastChild);
  149. // trailing whitespace inside a comment text node
  150. if (line.lastChild.children) {
  151. checkNode(line.lastChild.lastChild);
  152. }
  153. line.innerHTML = replaceWhitespace(line.innerHTML)
  154. // remove end CRLF if it exists
  155. .replace(regexCR, "") + whitespace.CRLF;
  156. max++;
  157. indx++;
  158. }
  159. if (indx < len) {
  160. setTimeout(() => {
  161. loop();
  162. }, 100);
  163. }
  164. };
  165. loop();
  166. }
  167. }
  168.  
  169. function updateTabSize(block) {
  170. // remove previous tab-size setting
  171. block.className = block.className.replace(regexTabSize, " ");
  172. // calculate tab-size; GitHub-Dark allows user modification
  173. const len = window.getComputedStyle($(".blob-code-inner", block)).tabSize;
  174. block.classList.add(`tab-size-${len}`);
  175. }
  176.  
  177. function $(selector, el) {
  178. return (el || document).querySelector(selector);
  179. }
  180.  
  181. function $$(selector, el) {
  182. return Array.from((el || document).querySelectorAll(selector));
  183. }
  184.  
  185. function closest(selector, el) {
  186. while (el && el.nodeType === 1) {
  187. if (el.matches(selector)) {
  188. return el;
  189. }
  190. el = el.parentNode;
  191. }
  192. return null;
  193. }
  194.  
  195. // bind whitespace toggle button
  196. document.addEventListener("click", event => {
  197. const target = event.target;
  198. if (
  199. target.nodeName === "DIV" &&
  200. target.classList.contains("ghcw-toggle")
  201. ) {
  202. let block = $(".highlight", closest(".file", target));
  203. if (block) {
  204. target.classList.toggle("selected");
  205. block.classList.toggle("ghcw-active");
  206. updateTabSize(block);
  207. addWhitespace(block);
  208. }
  209. }
  210. });
  211.  
  212. document.addEventListener("ghmo:container", addToggle);
  213. document.addEventListener("ghmo:diff", addToggle);
  214. // toggle added to diff & file view
  215. addToggle();
  216.  
  217. })();