GitHub Code Show Whitespace

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

当前为 2018-02-01 提交的版本,查看 最新版本

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