Skip to content

Instantly share code, notes, and snippets.

@AHilyard
Last active May 20, 2026 19:22
Show Gist options
  • Select an option

  • Save AHilyard/2687e9660d44cc21c84041d3fa687b98 to your computer and use it in GitHub Desktop.

Select an option

Save AHilyard/2687e9660d44cc21c84041d3fa687b98 to your computer and use it in GitHub Desktop.
Show whitespace on Github TamperMonkey script
// ==UserScript==
// @name GitHub Code Always Show Whitespace
// @version 2.0.0
// @description Always shows spaces, tabs, non-breaking spaces, and line endings in GitHub code/diff views.
// @license MIT
// @author Rob Garrison, AHilyard
// @match https://github.com/*
// @match https://gist.github.com/*
// @run-at document-idle
// @grant none
// @icon https://github.githubassets.com/pinned-octocat.svg
// ==/UserScript==
(() => {
"use strict";
const LINE_SELECTOR = [
".blob-code-inner:not(.blob-code-hunk)",
".react-code-text:not(.react-line-number)",
".diff-text-inner"
].join(",");
const WHITESPACE_REGEX = /[ \t\u00a0]/;
const STYLE = `
html.ghcw-active .ghcw-whitespace {
position: relative;
display: inline;
}
html.ghcw-active .ghcw-whitespace::before {
position: absolute;
opacity: .5;
user-select: none;
pointer-events: none;
font-weight: bold;
color: #777 !important;
top: -.25em;
left: 0;
}
html.ghcw-active .pl-space::before {
content: "\\b7";
}
html.ghcw-active .pl-nbsp::before {
content: "\\2423";
}
html.ghcw-active .pl-tab::before {
content: "\\bb";
}
html.ghcw-active .pl-crlf {
display: inline-block;
width: 0;
height: 1em;
vertical-align: baseline;
}
html.ghcw-active .pl-crlf::before {
content: "\\21b5";
top: .1em;
left: 0;
}
.blob-code-inner > br,
.react-code-text > br,
.diff-text-inner > br {
display: none !important;
}
`;
let queued = false;
let queue = [];
function addStyle() {
const style = document.createElement("style");
style.textContent = STYLE;
document.documentElement.appendChild(style);
document.documentElement.classList.add("ghcw-active");
}
function createWhitespaceSpan(character) {
const span = document.createElement("span");
span.classList.add("ghcw-whitespace");
if (character === " ") {
span.classList.add("pl-space");
span.textContent = " ";
} else if (character === "\t") {
span.classList.add("pl-tab");
span.textContent = "\t";
} else {
span.classList.add("pl-nbsp");
span.textContent = "\u00a0";
}
return span;
}
function createLineEndingSpan() {
const span = document.createElement("span");
span.className = "pl-crlf ghcw-whitespace";
span.textContent = "\u200b";
return span;
}
function shouldSkipTextNode(node) {
if (!node.nodeValue || !WHITESPACE_REGEX.test(node.nodeValue)) {
return true;
}
const parent = node.parentElement;
return Boolean(
parent &&
parent.closest(".ghcw-whitespace, textarea, input, script, style")
);
}
function removeRenderedLineBreaks(node) {
const walker = document.createTreeWalker(
node,
NodeFilter.SHOW_TEXT,
null
);
const textNodes = [];
let currentNode;
while ((currentNode = walker.nextNode())) {
textNodes.push(currentNode);
}
for (const textNode of textNodes) {
if (textNode.parentElement && textNode.parentElement.closest(".ghcw-whitespace")) {
continue;
}
if (textNode.nodeValue.indexOf("\n") !== -1 || textNode.nodeValue.indexOf("\r") !== -1) {
textNode.nodeValue = textNode.nodeValue.replace(/[\r\n]+/g, "");
}
}
for (const br of node.querySelectorAll(":scope > br")) {
br.remove();
}
}
function replaceTextNode(node) {
const fragment = document.createDocumentFragment();
const text = node.nodeValue;
for (let index = 0; index < text.length; index++) {
const character = text[index];
if (character === " " || character === "\t" || character === "\u00a0") {
fragment.appendChild(createWhitespaceSpan(character));
} else {
fragment.appendChild(document.createTextNode(character));
}
}
node.parentNode.replaceChild(fragment, node);
}
function processLine(line) {
if (!line || !line.isConnected) {
return;
}
removeRenderedLineBreaks(line);
const textNodes = [];
const walker = document.createTreeWalker(
line,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
return shouldSkipTextNode(node)
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
for (const textNode of textNodes) {
replaceTextNode(textNode);
}
if (!line.querySelector(".pl-crlf.ghcw-whitespace")) {
line.appendChild(createLineEndingSpan());
}
}
function enqueueLines(root) {
if (!root || root.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (root.matches(LINE_SELECTOR)) {
queue.push(root);
}
queue.push(...root.querySelectorAll(LINE_SELECTOR));
if (!queued) {
queued = true;
requestAnimationFrame(processQueue);
}
}
function processQueue() {
queued = false;
const lines = [...new Set(queue)];
queue = [];
let index = 0;
function chunk() {
const end = Math.min(index + 60, lines.length);
for (; index < end; index++) {
processLine(lines[index]);
}
if (index < lines.length) {
requestAnimationFrame(chunk);
}
}
chunk();
}
function observePage() {
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
enqueueLines(node);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function start() {
addStyle();
enqueueLines(document.body);
observePage();
}
if (document.body) {
start();
} else {
document.addEventListener("DOMContentLoaded", start, { once: true });
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment