Render Whitespace on GitHub

Renders spaces as · and tabs as → in all the code on GitHub.

As of 2017-09-23. See the latest version.

  1. /**
  2. The MIT License (MIT)
  3.  
  4. Copyright (c) 2017 Gleb Mazovetskiy
  5.  
  6. Permission is hereby granted, free of charge, to any person obtaining a copy of
  7. this software and associated documentation files (the "Software"), to deal in
  8. the Software without restriction, including without limitation the rights to
  9. use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  10. the Software, and to permit persons to whom the Software is furnished to do so,
  11. subject to the following conditions:
  12.  
  13. The above copyright notice and this permission notice shall be included in all
  14. copies or substantial portions of the Software.
  15.  
  16. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  18. FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  19. COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  20. IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  21. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. **/
  23. // ==UserScript==
  24. // @id RenderWhitespace
  25. // @name Render Whitespace on GitHub
  26. // @description Renders spaces as · and tabs as → in all the code on GitHub.
  27. // @namespace https://github.com/glebm
  28. // @version 1.3.3
  29. // @author Gleb Mazovetskiy <glex.spb@gmail.com>
  30. // @domain github.com
  31. // @domain gist.github.com
  32. // @match https://gist.github.com/*
  33. // @match https://github.com/*
  34. // @homepageUrl https://gist.github.com/5b6c4517322193fbc51090dc3b57a44a
  35. // @run-at document-end
  36. // @contributionURL https://etherchain.org/account/0x962644db6d8735446c1af84a2c1f16143f780184
  37. // ==/UserScript==
  38.  
  39.  
  40. // Settings
  41. var SPACE = '·';
  42. var TAB = '→';
  43. var WHITESPACE_OPACITY = 0.4;
  44. var COPYABLE_WHITESPACE_INDICATORS = false;
  45.  
  46. // Other constants
  47. var WS_CLASS = 'glebm-ws';
  48. var ROOT_SELECTOR = 'table[data-tab-size]';
  49. var NODE_FILTER = {
  50. acceptNode(node) {
  51. let parent = node.parentNode;
  52. if (parent.classList.contains(WS_CLASS)) return NodeFilter.FILTER_SKIP;
  53. while (parent.nodeName != 'TABLE') {
  54. if (parent.classList.contains('blob-code-inner')) {
  55. return !(parent.firstChild === node && node.nodeValue === ' ') ?
  56. NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  57. }
  58. parent = parent.parentNode;
  59. }
  60. return NodeFilter.FILTER_SKIP;
  61. }
  62. };
  63.  
  64. function main() {
  65. const styleNode = document.createElement('style');
  66. styleNode.textContent = COPYABLE_WHITESPACE_INDICATORS ?
  67. `.${WS_CLASS} { opacity: ${WHITESPACE_OPACITY}; }` :
  68. `.${WS_CLASS}::before {
  69. opacity: ${WHITESPACE_OPACITY};
  70. position: absolute;
  71. text-indent: 0;
  72. }` + /* In a diff: */
  73. `.blob-code > .blob-code-inner .${WS_CLASS}::before {
  74. line-height: 2;
  75. }`;
  76. document.head.appendChild(styleNode);
  77.  
  78. const registeredFragments = new WeakSet();
  79. const showWhitespaceOnNextTick = () => setTimeout(showWhitespace, 0);
  80. const initDOM = () => {
  81. showWhitespace();
  82. // https://github.com/github/include-fragment-element
  83. for (const node of document.querySelectorAll('include-fragment')) {
  84. if (registeredFragments.has(node)) continue;
  85. registeredFragments.add(node);
  86. node.addEventListener('loadend', showWhitespaceOnNextTick);
  87. }
  88. };
  89. document.addEventListener('pjax:success', initDOM);
  90. initDOM();
  91. }
  92.  
  93. function showWhitespace() {
  94. for (const root of document.querySelectorAll(ROOT_SELECTOR)) {
  95. const tab = TAB.padEnd(+root.dataset.tabSize);
  96. const treeWalker =
  97. document.createTreeWalker(root, NodeFilter.SHOW_TEXT, NODE_FILTER);
  98. const nodes = [];
  99. while (treeWalker.nextNode()) nodes.push(treeWalker.currentNode);
  100.  
  101. const isDiff = root.classList.contains('diff-table');
  102. for (const node of nodes) replaceWhitespace(node, tab, SPACE, isDiff);
  103. }
  104. }
  105.  
  106. function replaceWhitespace(node, tab, space, isDiff) {
  107. let originalText = node.nodeValue;
  108. const parent = node.parentNode;
  109. const ignoreFirstSpace = isDiff &&
  110. originalText.charAt(0) === ' ' &&
  111. parent.classList.contains('blob-code-inner') &&
  112. parent.firstChild === node;
  113. if (ignoreFirstSpace) {
  114. if (originalText === ' ') return;
  115. originalText = originalText.slice(1);
  116. parent.insertBefore(document.createTextNode(' '), node);
  117. }
  118. const tabParts = originalText.split('\t');
  119. const tabSpaceParts = tabParts.map(s => s.split(' '));
  120. if (!ignoreFirstSpace && tabSpaceParts.length === 1 &&
  121. tabSpaceParts[0].length === 1) return;
  122. const insert = (newNode) => {
  123. parent.insertBefore(newNode, node);
  124. };
  125. insertParts(tabSpaceParts,
  126. spaceParts => spaceParts.length === 1 && spaceParts[0] === '',
  127. n => insert(createWhitespaceNode('t', '\t', tab, n)),
  128. spaceParts =>
  129. insertParts(spaceParts,
  130. text => text === '',
  131. n => insert(createWhitespaceNode('s', ' ', space, n)),
  132. text => insert(document.createTextNode(text))));
  133. parent.removeChild(node);
  134. }
  135.  
  136.  
  137. var WS_ADDED_STYLES = new Set();
  138. function createWhitespaceNode(type, originalText, text, n) {
  139. const node = document.createElement('span');
  140. node.classList.add(WS_CLASS);
  141. if (COPYABLE_WHITESPACE_INDICATORS) {
  142. node.textContent = text.repeat(n);
  143. } else {
  144. const className = `${type}-${n}`;
  145. if (!WS_ADDED_STYLES.has(className)) {
  146. const styleNode = document.createElement('style');
  147. styleNode.textContent =
  148. `.${WS_CLASS}-${className}::before { content: '${text.repeat(n)}'; }`;
  149. document.head.appendChild(styleNode);
  150. WS_ADDED_STYLES.add(className);
  151. }
  152. node.classList.add(`${WS_CLASS}-${className}`);
  153. node.textContent = originalText.repeat(n);
  154. }
  155. return node;
  156. }
  157.  
  158. function insertParts(parts, isConsecutiveFn, addInterFn, addPartFn) {
  159. const n = parts.length;
  160. parts.reduce((consecutive, part, i) => {
  161. const isConsecutive = isConsecutiveFn(part);
  162. if (isConsecutive && i !== n - 1) return consecutive + 1;
  163. if (consecutive > 0) addInterFn(consecutive);
  164. if (!isConsecutive) addPartFn(part);
  165. return 1;
  166. }, 0);
  167. }
  168.  
  169. main();