GitHub Code Show Whitespace

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

当前为 2017-03-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 0.1.1
  4. // @description A userscript that shows whitespace (space, tabs and carriage returns) in code blocks
  5. // @license https://opensource.org/licenses/MIT
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @include https://gist.github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @author Rob Garrison
  12. // ==/UserScript==
  13. (() => {
  14. "use strict";
  15.  
  16. // include em-space & en-space?
  17. const whitespace = {
  18. "%20" : "<span class='pl-space ghcw-whitespace'> </span>",
  19. "%A0" : "<span class='pl-nbsp ghcw-whitespace'>&nbsp;</span>",
  20. "%09" : "<span class='pl-tab ghcw-whitespace'>\x09</span>",
  21. // non-matching key; applied manually
  22. "CRLF" : "<span class='pl-crlf ghcw-whitespace'></span>\n"
  23. },
  24. // ignore +/- in diff code blocks
  25. regexWS = /^(?:[+-]*)(\x20|&nbsp;|\x09)+/g,
  26. regexCR = /\r*\n$/,
  27. regexTabSize = /\btab-size-\d\b/g,
  28.  
  29. toggleButton = document.createElement("div");
  30. toggleButton.className = "ghcw-toggle btn btn-sm tooltipped tooltipped-n";
  31. toggleButton.setAttribute("aria-label", "Toggle Whitespace");
  32. toggleButton.innerHTML = "<span class='pl-tab'></span>";
  33.  
  34. GM_addStyle(`
  35. .highlight .blob-code-inner { tab-size: 2; }
  36. /* GitHub-Dark overrides the above setting */
  37. .highlight.ghcw-active.tab-size-2 .pl-tab { width: 1.1em; }
  38. .highlight.ghcw-active.tab-size-4 .pl-tab { width: 2.2em; }
  39. .highlight.ghcw-active.tab-size-6 .pl-tab { width: 3.3em; }
  40. .highlight.ghcw-active.tab-size-8 .pl-tab { width: 4.4em; }
  41.  
  42. .ghcw-active .ghcw-whitespace,
  43. .gist-content-wrapper .file-actions .btn-group {
  44. position: relative;
  45. display: inline-block;
  46. }
  47. .ghcw-active .ghcw-whitespace:before {
  48. position: absolute;
  49. overflow: hidden;
  50. opacity: .4;
  51. user-select: none;
  52. }
  53. .ghcw-toggle .pl-tab {
  54. pointer-events: none;
  55. }
  56. .ghcw-active .pl-space:before {
  57. content: "\\b7";
  58. }
  59. .ghcw-active .pl-nbsp:before {
  60. content: "\\b7";
  61. }
  62. .ghcw-active .pl-tab:before,
  63. .ghcw-toggle .pl-tab:before {
  64. content: "\\bb";
  65. }
  66. .ghcw-active .pl-crlf:before {
  67. content: "\\231d";
  68. top: -.75em;
  69. }
  70. `);
  71.  
  72. let debounce,
  73. busy = false,
  74. observers = [];
  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 addWhitespace(block) {
  85. let lines, indx, len;
  86. if (block && !block.classList.contains("ghcw-processed")) {
  87. block.classList.add("ghcw-processed");
  88. updateTabSize(block);
  89. indx = 0;
  90.  
  91. // class name of each code row
  92. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  93. len = lines.length;
  94.  
  95. // loop with delay to allow user interaction
  96. const loop = () => {
  97. let line,
  98. // max number of DOM insertions per loop
  99. max = 0;
  100. while (max < 50 && indx < len) {
  101. if (indx >= len) {
  102. return;
  103. }
  104. line = lines[indx];
  105. line.innerHTML = line.innerHTML
  106. .replace(regexWS, s => {
  107. let idx = 0,
  108. ln = s.length,
  109. result = "";
  110. for (idx = 0; idx < ln; idx++) {
  111. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  112. }
  113. return result;
  114. })
  115. // remove end CRLF if it exists
  116. .replace(regexCR, "") +
  117. whitespace.CRLF;
  118. max++;
  119. indx++;
  120. }
  121. if (indx < len) {
  122. setTimeout(() => {
  123. loop();
  124. }, 200);
  125. }
  126. };
  127. loop();
  128. }
  129. }
  130.  
  131. function updateTabSize(block) {
  132. // remove previous tab-size setting
  133. block.className = block.className.replace(regexTabSize, " ");
  134. // calculate tab-size; GitHub-Dark allows user modification
  135. const len = window.getComputedStyle($(".blob-code-inner", block)).tabSize;
  136. block.classList.add(`tab-size-${len}`);
  137. }
  138.  
  139. function removeObservers() {
  140. // remove saved observers
  141. observers.forEach(observer => {
  142. if (observer) {
  143. observer.disconnect();
  144. }
  145. });
  146. observers = [];
  147. }
  148.  
  149. function addObservers() {
  150. // Observe progressively loaded content
  151. $$(`
  152. .js-diff-progressive-container,
  153. .js-diff-load-container`
  154. ).forEach(target => {
  155. const obsrvr = new MutationObserver(mutations => {
  156. mutations.forEach(mutation => {
  157. // preform checks before adding code wrap to minimize function calls
  158. const tar = mutation.target;
  159. if (!busy && tar && (
  160. tar.classList.contains("js-diff-progressive-container") ||
  161. tar.classList.contains("js-diff-load-container") ||
  162. tar.classList.contains("blob-wrapper")
  163. )
  164. ) {
  165. clearTimeout(debounce);
  166. debounce = setTimeout(() => {
  167. addToggle();
  168. }, 500);
  169. }
  170. });
  171. });
  172. obsrvr.observe(target, {
  173. childList: true,
  174. subtree: true
  175. });
  176. observers.push(obsrvr);
  177. });
  178. }
  179.  
  180. function init() {
  181. // toggle added to diff & file view
  182. addToggle();
  183. removeObservers();
  184. if (document.querySelector("#files.diff-view")) {
  185. addObservers();
  186. }
  187. }
  188.  
  189. function $(selector, el) {
  190. return (el || document).querySelector(selector);
  191. }
  192.  
  193. function $$(selector, el) {
  194. return Array.from((el || document).querySelectorAll(selector));
  195. }
  196.  
  197. function closest(selector, el) {
  198. while (el && el.nodeType === 1) {
  199. if (el.matches(selector)) {
  200. return el;
  201. }
  202. el = el.parentNode;
  203. }
  204. return null;
  205. }
  206.  
  207. // bind whitespace toggle button
  208. document.addEventListener("click", event => {
  209. const target = event.target;
  210. if (
  211. target.nodeName === "DIV" &&
  212. target.classList.contains("ghcw-toggle")
  213. ) {
  214. let block = $(".highlight", closest(".file", target));
  215. if (block) {
  216. target.classList.toggle("selected");
  217. block.classList.toggle("ghcw-active");
  218. updateTabSize(block);
  219. addWhitespace(block);
  220. }
  221. }
  222. });
  223. document.addEventListener("pjax:end", init);
  224.  
  225. setTimeout(() => {
  226. init();
  227. }, 500);
  228.  
  229. })();