GitHub Code Show Whitespace

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

目前為 2017-09-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Show Whitespace
  3. // @version 0.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. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=189706
  13. // @icon https://github.com/fluidicon.png
  14. // ==/UserScript==
  15. (() => {
  16. "use strict";
  17.  
  18. // include em-space & en-space?
  19. const whitespace = {
  20. "%20" : "<span class='pl-space ghcw-whitespace'> </span>",
  21. "%A0" : "<span class='pl-nbsp ghcw-whitespace'>&nbsp;</span>",
  22. "%09" : "<span class='pl-tab ghcw-whitespace'>\x09</span>",
  23. // non-matching key; applied manually
  24. "CRLF" : "<span class='pl-crlf ghcw-whitespace'></span>\n"
  25. },
  26. // ignore +/- in diff code blocks
  27. regexWS = /^(?:[+-]*)(\x20|&nbsp;|\x09)+/g,
  28. regexCR = /\r*\n$/,
  29. regexTabSize = /\btab-size-\d\b/g,
  30.  
  31. toggleButton = document.createElement("div");
  32. toggleButton.className = "ghcw-toggle btn btn-sm tooltipped tooltipped-n";
  33. toggleButton.setAttribute("aria-label", "Toggle Whitespace");
  34. toggleButton.innerHTML = "<span class='pl-tab'></span>";
  35.  
  36. GM_addStyle(`
  37. .highlight .blob-code-inner { tab-size: 2; }
  38. /* GitHub-Dark overrides the above setting */
  39. .highlight.ghcw-active.tab-size-2 .pl-tab { width: 1.1em; }
  40. .highlight.ghcw-active.tab-size-4 .pl-tab { width: 2.2em; }
  41. .highlight.ghcw-active.tab-size-6 .pl-tab { width: 3.3em; }
  42. .highlight.ghcw-active.tab-size-8 .pl-tab { width: 4.4em; }
  43.  
  44. .ghcw-active .ghcw-whitespace,
  45. .gist-content-wrapper .file-actions .btn-group {
  46. position: relative;
  47. display: inline-block;
  48. }
  49. .ghcw-active .ghcw-whitespace:before {
  50. position: absolute;
  51. overflow: hidden;
  52. opacity: .4;
  53. user-select: none;
  54. }
  55. .ghcw-toggle .pl-tab {
  56. pointer-events: none;
  57. }
  58. .ghcw-active .pl-space:before {
  59. content: "\\b7";
  60. }
  61. .ghcw-active .pl-nbsp:before {
  62. content: "\\b7";
  63. }
  64. .ghcw-active .pl-tab:before,
  65. .ghcw-toggle .pl-tab:before {
  66. content: "\\bb";
  67. }
  68. .ghcw-active .pl-crlf:before {
  69. content: "\\231d";
  70. top: -.75em;
  71. }
  72. `);
  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 replaceWhitespace(html) {
  83. return html
  84. .replace(regexWS, s => {
  85. let idx = 0,
  86. ln = s.length,
  87. result = "";
  88. for (idx = 0; idx < ln; idx++) {
  89. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  90. }
  91. return result;
  92. });
  93. }
  94.  
  95. function addWhitespace(block) {
  96. let lines, indx, len;
  97. if (block && !block.classList.contains("ghcw-processed")) {
  98. block.classList.add("ghcw-processed");
  99. updateTabSize(block);
  100. indx = 0;
  101.  
  102. // class name of each code row
  103. lines = $$(".blob-code-inner:not(.blob-code-hunk)", block);
  104. len = lines.length;
  105.  
  106. // loop with delay to allow user interaction
  107. const loop = () => {
  108. let el, line,
  109. // max number of DOM insertions per loop
  110. max = 0;
  111. while (max < 50 && indx < len) {
  112. if (indx >= len) {
  113. return;
  114. }
  115. line = lines[indx];
  116. el = line.firstChild;
  117. // first node is a syntax string and may have leading whitespace
  118. if (
  119. el &&
  120. el.nodeType === 1 &&
  121. el.classList.contains("pl-s") &&
  122. el.firstChild.nodeType === 3
  123. ) {
  124. el.innerHTML = replaceWhitespace(el.innerHTML);
  125. }
  126. line.innerHTML = replaceWhitespace(line.innerHTML)
  127. // remove end CRLF if it exists
  128. .replace(regexCR, "") + whitespace.CRLF;
  129. max++;
  130. indx++;
  131. }
  132. if (indx < len) {
  133. setTimeout(() => {
  134. loop();
  135. }, 100);
  136. }
  137. };
  138. loop();
  139. }
  140. }
  141.  
  142. function updateTabSize(block) {
  143. // remove previous tab-size setting
  144. block.className = block.className.replace(regexTabSize, " ");
  145. // calculate tab-size; GitHub-Dark allows user modification
  146. const len = window.getComputedStyle($(".blob-code-inner", block)).tabSize;
  147. block.classList.add(`tab-size-${len}`);
  148. }
  149.  
  150. function $(selector, el) {
  151. return (el || document).querySelector(selector);
  152. }
  153.  
  154. function $$(selector, el) {
  155. return Array.from((el || document).querySelectorAll(selector));
  156. }
  157.  
  158. function closest(selector, el) {
  159. while (el && el.nodeType === 1) {
  160. if (el.matches(selector)) {
  161. return el;
  162. }
  163. el = el.parentNode;
  164. }
  165. return null;
  166. }
  167.  
  168. // bind whitespace toggle button
  169. document.addEventListener("click", event => {
  170. const target = event.target;
  171. if (
  172. target.nodeName === "DIV" &&
  173. target.classList.contains("ghcw-toggle")
  174. ) {
  175. let block = $(".highlight", closest(".file", target));
  176. if (block) {
  177. target.classList.toggle("selected");
  178. block.classList.toggle("ghcw-active");
  179. updateTabSize(block);
  180. addWhitespace(block);
  181. }
  182. }
  183. });
  184.  
  185. document.addEventListener("ghmo:container", addToggle);
  186. document.addEventListener("ghmo:diff", addToggle);
  187. // toggle added to diff & file view
  188. addToggle();
  189.  
  190. })();