GitLab Code Show Whitespace

A userscript that shows whitespace (space, tabs and carriage returns) in code blocks based on https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-code-show-whitespace (without mutations)

  1. // ==UserScript==
  2. // @name GitLab Code Show Whitespace
  3. // @version 1.1.10-a
  4. // @description A userscript that shows whitespace (space, tabs and carriage returns) in code blocks based on https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-code-show-whitespace (without mutations)
  5. // @license MIT
  6. // @author Nikolai Merinov
  7. // @namespace https://github.com/mnd
  8. // @include https://gitlab.*
  9. // @run-at document-idle
  10. // @grant GM.addStyle
  11. // @grant GM_addStyle
  12. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
  13. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  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>"
  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 has-tooltip btn-file-option";
  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. top: .1em;
  70. }
  71. .ghcw-active .pl-crlf:before {
  72. content: "\\231d";
  73. }
  74. /* weird tweak for diff markdown files - see #27 */
  75. .ghcw-adjust .ghcw-active .ghcw-whitespace:before {
  76. left: .6em;
  77. }
  78. /* hide extra leading space added to diffs - see #27 */
  79. .text-file td.line .pl-space:first-child {
  80. opacity: 0;
  81. }
  82. `);
  83.  
  84. function $(selector, el) {
  85. return (el || document).querySelector(selector);
  86. }
  87.  
  88. function $$(selector, el) {
  89. return [...(el || document).querySelectorAll(selector)];
  90. }
  91.  
  92. function addToggle() {
  93. $$(".file-actions").forEach(el => {
  94. if (!$(".ghcw-toggle", el)) {
  95. el.insertBefore(toggleButton.cloneNode(true), el.childNodes[0]);
  96. }
  97. });
  98. }
  99.  
  100. function getNodes(line) {
  101. const nodeIterator = document.createNodeIterator(
  102. line,
  103. NodeFilter.SHOW_TEXT,
  104. () => NodeFilter.FILTER_ACCEPT
  105. );
  106. let currentNode,
  107. nodes = [];
  108. while ((currentNode = nodeIterator.nextNode())) {
  109. nodes.push(currentNode);
  110. }
  111. return nodes;
  112. }
  113.  
  114. function escapeHTML(html) {
  115. return html.replace(/[<>"'&]/g, m => ({
  116. "<": "&lt;",
  117. ">": "&gt;",
  118. "&": "&amp;",
  119. "'": "&#39;",
  120. "\"": "&quot;"
  121. }[m]));
  122. }
  123.  
  124. function replaceWhitespace(html) {
  125. return escapeHTML(html).replace(regexWS, s => {
  126. let idx = 0,
  127. ln = s.length,
  128. result = "";
  129. for (idx = 0; idx < ln; idx++) {
  130. result += whitespace[encodeURI(s[idx])] || s[idx] || "";
  131. }
  132. return result;
  133. });
  134. }
  135.  
  136. function replaceTextNode(nodes) {
  137. let node, indx, el,
  138. ln = nodes.length;
  139. for (indx = 0; indx < ln; indx++) {
  140. node = nodes[indx];
  141. if (
  142. node &&
  143. node.nodeType === 3 &&
  144. node.textContent &&
  145. node.textContent.search(regexWS) > -1
  146. ) {
  147. el = span.cloneNode();
  148. el.innerHTML = replaceWhitespace(node.textContent.replace(regexCR, ""));
  149. node.parentNode.insertBefore(el, node);
  150. node.parentNode.removeChild(node);
  151. }
  152. }
  153. }
  154.  
  155. function addWhitespace(block) {
  156. let lines, indx, len;
  157. if (block && !block.classList.contains("ghcw-processed")) {
  158. block.classList.add("ghcw-processed");
  159. indx = 0;
  160.  
  161. // class name of each code row
  162. lines = $$(".line", block);
  163. len = lines.length;
  164.  
  165. // loop with delay to allow user interaction
  166. const loop = () => {
  167. let line, nodes,
  168. // max number of DOM insertions per loop
  169. max = 0;
  170. while (max < 50 && indx < len) {
  171. if (indx >= len) {
  172. return;
  173. }
  174. line = lines[indx];
  175. // first node is a syntax string and may have leading whitespace
  176. nodes = getNodes(line);
  177. replaceTextNode(nodes);
  178. // remove end CRLF if it exists; then add a line ending
  179. line.innerHTML = line.innerHTML.replace(regexCR, "") + whitespace.CRLF;
  180. max++;
  181. indx++;
  182. }
  183. if (indx < len) {
  184. setTimeout(() => {
  185. loop();
  186. }, 100);
  187. }
  188. };
  189. loop();
  190. }
  191. }
  192.  
  193. function detectDiff(wrap) {
  194. const header = $(".btn-clipboard", wrap);
  195. if ($(".diff-content", wrap) && header) {
  196. const file = header.getAttribute("data-clipboard-text");
  197. if (
  198. // File Exceptions that need tweaking (e.g. ".md")
  199. regexExceptions.test(file) ||
  200. // files with no extension (e.g. LICENSE)
  201. file.indexOf(".") === -1
  202. ) {
  203. // This class is added to adjust the position of the whitespace
  204. // markers for specific files; See issue #27
  205. wrap.classList.add("ghcw-adjust");
  206. }
  207. }
  208. }
  209.  
  210. // bind whitespace toggle button
  211. document.addEventListener("click", event => {
  212. const target = event.target;
  213. if (
  214. target.nodeName === "DIV" &&
  215. target.classList.contains("ghcw-toggle")
  216. ) {
  217. const wrap = target.closest(".diff-file");
  218. const block = $(".text-file", wrap); // diff-content
  219. if (block) {
  220. target.classList.toggle("selected");
  221. block.classList.toggle("ghcw-active");
  222. detectDiff(wrap);
  223. addWhitespace(block);
  224. }
  225. }
  226. });
  227.  
  228. // Add toggle every second to be sure that it will start after each update
  229. setInterval(addToggle, 1000);
  230. // toggle added to diff & file view
  231. addToggle();
  232. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址