GitHub Code Show Whitespace

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

当前为 2019-02-18 提交的版本,查看 最新版本

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