GitHub Code Show Whitespace

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

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

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