GitHub Code Show Whitespace

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

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

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 1.1.4
  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=234970
  13. // @icon https://user-images.githubusercontent.com/136959/33633295-f1547d62-d9d5-11e7-9fa1-f2777ec3c745.png
  14. // ==/UserScript==
  15. (() => {
  16. "use strict";
  17.  
  18. // include em-space & en-space?
  19. const whitespace = {
  20. // Applies \xb7 (·) to every space
  21. "%20" : "<span class='pl-space ghcw-whitespace'> </span>",
  22. // Applies \xb7 (·) to every non-breaking space (alternative: \u2423 (␣))
  23. "%A0" : "<span class='pl-nbsp ghcw-whitespace'>&nbsp;</span>",
  24. // Applies \xbb (») to every tab
  25. "%09" : "<span class='pl-tab ghcw-whitespace'>\x09</span>",
  26. // non-matching key; applied manually
  27. // Applies \u231d (⌝) to the end of every line
  28. // (alternatives: \u21b5 (↵) or \u2938 (⤸))
  29. "CRLF" : "<span class='pl-crlf ghcw-whitespace'></span>\n"
  30. },
  31. span = document.createElement("span"),
  32. // ignore +/- in diff code blocks
  33. regexWS = /(\x20|&nbsp;|\x09)/g,
  34. regexCR = /\r*\n$/,
  35. regexExceptions = /(\.md)$/i,
  36.  
  37. toggleButton = document.createElement("div");
  38. toggleButton.className = "ghcw-toggle btn btn-sm tooltipped tooltipped-n";
  39. toggleButton.setAttribute("aria-label", "Toggle Whitespace");
  40. toggleButton.innerHTML = "<span class='pl-tab'></span>";
  41.  
  42. GM_addStyle(`
  43. .ghcw-active .ghcw-whitespace,
  44. .gist-content-wrapper .file-actions .btn-group {
  45. position: relative;
  46. display: inline;
  47. }
  48. .ghcw-active .ghcw-whitespace:before {
  49. position: absolute;
  50. opacity: .5;
  51. user-select: none;
  52. font-weight: bold;
  53. color: #777 !important;
  54. top: -.25em;
  55. left: 0;
  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: .1em;
  73. }
  74. /* weird tweak for diff markdown files - see #27 */
  75. .ghcw-adjust .ghcw-active .ghcw-whitespace:before {
  76. left: .6em;
  77. }
  78. /* hide extra leading space added to diffs - see #27 */
  79. .diff-table td.blob-code-inner .pl-space:first-child,
  80. .diff-table .blob-code-context .pl-space:first-child {
  81. opacity: 0;
  82. }
  83. `);
  84.  
  85. function addToggle() {
  86. $$(".file-actions").forEach(el => {
  87. if (!$(".ghcw-toggle", el)) {
  88. el.insertBefore(toggleButton.cloneNode(true), el.childNodes[0]);
  89. }
  90. });
  91. }
  92.  
  93. function getNodes(line) {
  94. const nodeIterator = document.createNodeIterator(
  95. line,
  96. NodeFilter.SHOW_TEXT,
  97. () => NodeFilter.FILTER_ACCEPT
  98. );
  99. let currentNode,
  100. nodes = [];
  101. while ((currentNode = nodeIterator.nextNode())) {
  102. nodes.push(currentNode);
  103. }
  104. return nodes;
  105. }
  106.  
  107. function escapeHTML(html) {
  108. return html.replace(/[<>"'&]/g, m => ({
  109. "<": "&lt;",
  110. ">": "&gt;",
  111. "&": "&amp;",
  112. "'": "&#39;",
  113. "\"": "&quot;"
  114. }[m]));
  115. }
  116.  
  117. function replaceWhitespace(html) {
  118. return escapeHTML(html).replace(regexWS, s => {
  119. let idx = 0,
  120. ln = s.length,
  121. result = "";
  122. for (idx = 0; idx < ln; idx++) {
  123. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  124. }
  125. return result;
  126. });
  127. }
  128.  
  129. function replaceTextNode(nodes) {
  130. let node, indx, el,
  131. ln = nodes.length;
  132. for (indx = 0; indx < ln; indx++) {
  133. node = nodes[indx];
  134. if (
  135. node &&
  136. node.nodeType === 3 &&
  137. node.textContent &&
  138. node.textContent.search(regexWS) > -1
  139. ) {
  140. el = span.cloneNode();
  141. el.innerHTML = replaceWhitespace(node.textContent.replace(regexCR, ""));
  142. node.parentNode.insertBefore(el, node);
  143. node.parentNode.removeChild(node);
  144. }
  145. }
  146. }
  147.  
  148. function addWhitespace(block) {
  149. let lines, indx, len;
  150. if (block && !block.classList.contains("ghcw-processed")) {
  151. block.classList.add("ghcw-processed");
  152. indx = 0;
  153.  
  154. // class name of each code row
  155. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  156. len = lines.length;
  157.  
  158. // loop with delay to allow user interaction
  159. const loop = () => {
  160. let line, nodes,
  161. // max number of DOM insertions per loop
  162. max = 0;
  163. while (max < 50 && indx < len) {
  164. if (indx >= len) {
  165. return;
  166. }
  167. line = lines[indx];
  168. // first node is a syntax string and may have leading whitespace
  169. nodes = getNodes(line);
  170. replaceTextNode(nodes);
  171. // remove end CRLF if it exists; then add a line ending
  172. line.innerHTML = line.innerHTML.replace(regexCR, "") + whitespace.CRLF;
  173. max++;
  174. indx++;
  175. }
  176. if (indx < len) {
  177. setTimeout(() => {
  178. loop();
  179. }, 100);
  180. }
  181. };
  182. loop();
  183. }
  184. }
  185.  
  186. function detectDiff(wrap) {
  187. const header = $(".file-header", wrap);
  188. if ($(".diff-table", wrap) && header) {
  189. const file = header.getAttribute("data-path");
  190. if (
  191. // File Exceptions that need tweaking (e.g. ".md")
  192. regexExceptions.test(file) ||
  193. // files with no extension (e.g. LICENSE)
  194. file.indexOf(".") === -1
  195. ) {
  196. // This class is added to adjust the position of the whitespace
  197. // markers for specific files; See issue #27
  198. wrap.classList.add("ghcw-adjust");
  199. }
  200. }
  201. }
  202.  
  203. function $(selector, el) {
  204. return (el || document).querySelector(selector);
  205. }
  206.  
  207. function $$(selector, el) {
  208. return [...(el || document).querySelectorAll(selector)];
  209. }
  210.  
  211. function closest(selector, el) {
  212. while (el && el.nodeType === 1) {
  213. if (el.matches(selector)) {
  214. return el;
  215. }
  216. el = el.parentNode;
  217. }
  218. return null;
  219. }
  220.  
  221. // bind whitespace toggle button
  222. document.addEventListener("click", event => {
  223. const target = event.target;
  224. if (
  225. target.nodeName === "DIV" &&
  226. target.classList.contains("ghcw-toggle")
  227. ) {
  228. const wrap = closest(".file", target);
  229. const block = $(".highlight", wrap);
  230. if (block) {
  231. target.classList.toggle("selected");
  232. block.classList.toggle("ghcw-active");
  233. detectDiff(wrap);
  234. addWhitespace(block);
  235. }
  236. }
  237. });
  238.  
  239. document.addEventListener("ghmo:container", addToggle);
  240. document.addEventListener("ghmo:diff", addToggle);
  241. // toggle added to diff & file view
  242. addToggle();
  243.  
  244. })();