Greasy Fork is available in English.

GitHub Code Show Whitespace

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

Per 03-10-2017. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 1.1.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://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=198500
  13. // @icon https://github.com/fluidicon.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. `);
  79.  
  80. function addToggle() {
  81. $$(".file-actions").forEach(el => {
  82. if (!$(".ghcw-toggle", el)) {
  83. el.insertBefore(toggleButton.cloneNode(true), el.childNodes[0]);
  84. }
  85. });
  86. }
  87.  
  88. function getNodes(line) {
  89. const nodeIterator = document.createNodeIterator(
  90. line,
  91. NodeFilter.SHOW_TEXT,
  92. () => NodeFilter.FILTER_ACCEPT
  93. );
  94. let currentNode,
  95. nodes = [];
  96. while ((currentNode = nodeIterator.nextNode())) {
  97. nodes.push(currentNode);
  98. }
  99. return nodes;
  100. }
  101.  
  102. function escapeHTML(html) {
  103. return html.replace(/[<>"'&]/g, m => ({
  104. "<": "&lt;",
  105. ">": "&gt;",
  106. "&": "&amp;",
  107. "'": "&#39;",
  108. "\"": "&quot;"
  109. }[m]));
  110. }
  111.  
  112. function replaceWhitespace(html) {
  113. return escapeHTML(html).replace(regexWS, s => {
  114. let idx = 0,
  115. ln = s.length,
  116. result = "";
  117. for (idx = 0; idx < ln; idx++) {
  118. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  119. }
  120. return result;
  121. });
  122. }
  123.  
  124. function replaceTextNode(nodes) {
  125. let node, indx, el,
  126. ln = nodes.length;
  127. for (indx = 0; indx < ln; indx++) {
  128. node = nodes[indx];
  129. if (
  130. node &&
  131. node.nodeType === 3 &&
  132. node.textContent &&
  133. node.textContent.search(regexWS) > -1
  134. ) {
  135. el = span.cloneNode();
  136. el.innerHTML = replaceWhitespace(node.textContent.replace(regexCR, ""));
  137. node.parentNode.insertBefore(el, node);
  138. node.parentNode.removeChild(node);
  139. }
  140. }
  141. }
  142.  
  143. function addWhitespace(block) {
  144. let lines, indx, len;
  145. if (block && !block.classList.contains("ghcw-processed")) {
  146. block.classList.add("ghcw-processed");
  147. indx = 0;
  148.  
  149. // class name of each code row
  150. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  151. len = lines.length;
  152.  
  153. // loop with delay to allow user interaction
  154. const loop = () => {
  155. let line, nodes,
  156. // max number of DOM insertions per loop
  157. max = 0;
  158. while (max < 50 && indx < len) {
  159. if (indx >= len) {
  160. return;
  161. }
  162. line = lines[indx];
  163. // first node is a syntax string and may have leading whitespace
  164. nodes = getNodes(line);
  165. replaceTextNode(nodes);
  166. // remove end CRLF if it exists; then add a line ending
  167. line.innerHTML = line.innerHTML.replace(regexCR, "") + whitespace.CRLF;
  168. max++;
  169. indx++;
  170. }
  171. if (indx < len) {
  172. setTimeout(() => {
  173. loop();
  174. }, 100);
  175. }
  176. };
  177. loop();
  178. }
  179. }
  180.  
  181. function detectDiff(wrap) {
  182. const header = $(".file-header", wrap);
  183. if ($(".diff-table", wrap) && header) {
  184. const file = header.getAttribute("data-path");
  185. if (
  186. // File Exceptions that need tweaking (e.g. ".md")
  187. regexExceptions.test(file) ||
  188. // files with no extension (e.g. LICENSE)
  189. file.indexOf(".") === -1
  190. ) {
  191. // This class is added to adjust the position of the whitespace
  192. // markers for specific files; See issue #27
  193. wrap.classList.add("ghcw-adjust");
  194. }
  195. }
  196. }
  197.  
  198. function $(selector, el) {
  199. return (el || document).querySelector(selector);
  200. }
  201.  
  202. function $$(selector, el) {
  203. return [...(el || document).querySelectorAll(selector)];
  204. }
  205.  
  206. function closest(selector, el) {
  207. while (el && el.nodeType === 1) {
  208. if (el.matches(selector)) {
  209. return el;
  210. }
  211. el = el.parentNode;
  212. }
  213. return null;
  214. }
  215.  
  216. // bind whitespace toggle button
  217. document.addEventListener("click", event => {
  218. const target = event.target;
  219. if (
  220. target.nodeName === "DIV" &&
  221. target.classList.contains("ghcw-toggle")
  222. ) {
  223. const wrap = closest(".file", target);
  224. const block = $(".highlight", wrap);
  225. if (block) {
  226. target.classList.toggle("selected");
  227. block.classList.toggle("ghcw-active");
  228. detectDiff(wrap);
  229. addWhitespace(block);
  230. }
  231. }
  232. });
  233.  
  234. document.addEventListener("ghmo:container", addToggle);
  235. document.addEventListener("ghmo:diff", addToggle);
  236. // toggle added to diff & file view
  237. addToggle();
  238.  
  239. })();