GitHub Code Colors

A userscript that adds a color swatch next to the code color definition

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        GitHub Code Colors
// @version     2.0.9
// @description A userscript that adds a color swatch next to the code color definition
// @license     MIT
// @author      Rob Garrison
// @namespace   https://github.com/Mottie
// @match       https://github.com/*
// @match       https://gist.github.com/*
// @run-at      document-idle
// @grant       GM.addStyle
// @grant       GM_addStyle
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
// @require     https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @require     https://greasyfork.org/scripts/387811-color-bundle/code/color-bundle.js?version=719499
// @icon        https://github.githubassets.com/pinned-octocat.svg
// @supportURL  https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==

/* global Color */
(() => {
	"use strict";

	// whitespace:initial => overrides code-wrap css in content
	GM.addStyle(`
	.ghcc-block { width:14px; height:14px; display:inline-block;
		vertical-align:middle; margin-right:4px; border-radius:4px;
		border:1px solid rgba(119, 119, 119, 0.5); position:relative;
		background-image:none; cursor:pointer; }
	.ghcc-popup { position:absolute; background:#222; color:#eee;
		min-width:350px; top:100%; left:0px; padding:10px; z-index:100;
		white-space:pre; cursor:text; text-align:left; -webkit-user-select:text;
		-moz-user-select:text; -ms-user-select:text; user-select:text; }
	.markdown-body .highlight pre, .markdown-body pre {
		overflow-y:visible !important; }
	.ghcc-copy { padding:2px 6px; margin-right:4px; background:transparent;
		border:0; }`);

	const namedColors = Object.keys(Color.namedColors);
	const namedColorsList = namedColors.reduce((acc, name) => {
		acc[name] = `rgb(${Color.namedColors[name].join(", ")})`;
		return acc;
	}, {});

	const copyButton = document.createElement("clipboard-copy");
	copyButton.className = "btn btn-sm btn-blue tooltipped tooltipped-w ghcc-copy";
	copyButton.setAttribute("aria-label", "Copy to clipboard");
	// This hint isn't working yet (GitHub needs to fix it)
	copyButton.setAttribute("data-copied-hint", "Copied!");
	copyButton.innerHTML = `
		<svg aria-hidden="true" class="octicon octicon-clippy" height="14" viewBox="0 0 14 16" width="14">
			<path fill-rule="evenodd" d="M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z"></path>
		</svg>`;

	// Misc regex
	const regex = {
		quotes: /['"]/g,
		unix: /^0x/,
		percent: /%%/g
	};

	// Don't use a div, because GitHub-Dark adds a :hover background
	// color definition on divs
	const block = document.createElement("button");
	block.className = "ghcc-block";
	block.tabIndex = 0;
	// prevent submitting on click in comment preview
	block.type = "button";
	block.onclick = "event => event.stopPropagation()";

	const br = document.createElement("br");

	const popup = document.createElement("span");
	popup.className = "ghcc-popup";

	const formats = {
		named: {
			regex: new RegExp("^(" + namedColors.join("|") + ")$", "i"),
			convert: color => {
				const rgb = color.rgb().toString();
				if (Object.values(namedColorsList).includes(rgb)) {
					// There may be more than one named color
					// e.g. "slategray" & "slategrey"
					return Object.keys(namedColorsList)
						.filter(n => namedColorsList[n] === rgb)
						.join("<br />");
				}
				return "";
			},
		},
		hex: {
			// Ex: #123, #123456 or 0x123456 (unix style colors, used by three.js)
			regex: /^(#|0x)([0-9A-F]{6,8}|[0-9A-F]{3,4})$/i,
			convert: color => `${color.hex().toString()}`,
		},
		rgb: {
			regex: /^rgba?(\([^\)]+\))?/i,
			regexAlpha: /rgba/i,
			find: (els, el, txt) => {
				// Color in a string contains everything
				if (el.classList.contains("pl-s")) {
					txt = txt.match(formats.rgb.regex)[0];
				} else {
					// Rgb(a) colors contained in multiple "pl-c1" spans
					let indx = formats.rgb.regexAlpha.test(txt) ? 4 : 3;
					const tmp = [];
					while (indx) {
						tmp.push(getTextContent(els.shift()));
						indx--;
					}
					txt += "(" + tmp.join(",") + ")";
				}
				addNode(el, txt);
				return els;
			},
			convert: color => {
				const rgb = color.rgb().alpha(1).toString();
				const rgba = color.rgb().toString();
				return `${rgb}${rgb === rgba ? "" : "; " + rgba}`;
			}
		},
		hsl: {
			// Ex: hsl(0,0%,0%) or hsla(0,0%,0%,0.2);
			regex: /^hsla?(\([^\)]+\))?/i,
			find: (els, el, txt) => {
				const tmp = /a$/i.test(txt);
				if (el.classList.contains("pl-s")) {
					// Color in a string contains everything
					txt = txt.match(formats.hsl.regex)[0];
				} else {
					// Traverse this HTML... & els only contains the pl-c1 nodes
					// <span class="pl-c1">hsl</span>(<span class="pl-c1">1</span>,
					// <span class="pl-c1">1</span><span class="pl-k">%</span>,
					// <span class="pl-c1">1</span><span class="pl-k">%</span>);
					// using getTextContent in case of invalid css
					txt = txt + "(" + getTextContent(els.shift()) + "," +
						getTextContent(els.shift()) + "%," +
						// Hsla needs one more parameter
						getTextContent(els.shift()) + "%" +
						(tmp ? "," + getTextContent(els.shift()) : "") + ")";
				}
				// Sometimes (previews only?) the .pl-k span is nested inside
				// the .pl-c1 span, so we end up with "%%"
				addNode(el, txt.replace(regex.percent, "%"));
				return els;
			},
			convert: color => {
				const hsl = color.hsl().alpha(1).round().toString();
				const hsla = color.hsl().round().toString();
				return `${hsl}${hsl === hsla ? "" : "; " + hsla}`;
			}
		},
		hwb: {
			convert: color => color.hwb().round().toString()
		},
		cymk: {
			convert: color => {
				const cmyk = color.cmyk().round().array(); // array of numbers
				return `device-cmyk(${cmyk.shift()}, ${cmyk.join("%, ")})`;
			}
		},
	};

	function showPopup(el) {
		const popup = createPopup(el.style.backgroundColor);
		el.appendChild(popup);
	}

	function hidePopup(el) {
		el.textContent = "";
	}

	function checkPopup(event) {
		const el = event.target;
		if (el && el.classList.contains("ghcc-block")) {
			if (event.type === "click") {
				if (el.textContent) {
					hidePopup(el)
				} else {
					showPopup(el);
				}
			}
		}
		if (event.type === "keyup" && event.key === "Escape") {
			// hide all popups
			[...document.querySelectorAll(".ghcc-block")].forEach(el => {
				el.textContent = "";
			});
		}
	}

	function createPopup(val) {
		const color = Color(val);
		const el = popup.cloneNode();
		const fragment = document.createDocumentFragment();
		Object.keys(formats).forEach(type => {
			if (typeof formats[type].convert === "function") {
				const val = formats[type].convert(color);
				if (val) {
					const button = copyButton.cloneNode(true);
					button.value = val;
					fragment.appendChild(button);
					fragment.appendChild(document.createTextNode(val));
					fragment.appendChild(br.cloneNode());
				}
			}
		});
		el.appendChild(fragment);
		return el;
	}

	function addNode(el, val) {
		const node = block.cloneNode();
		node.style.backgroundColor = val;
		// Don't add node if color is invalid
		if (node.style.backgroundColor !== "") {
			el.insertBefore(node, el.childNodes[0]);
		}
	}

	function getTextContent(el) {
		return el ? el.textContent : "";
	}

	// Loop with delay to allow user interaction
	function* addBlock(els) {
		let last = "";
		while (els.length) {
			let el = els.shift();
			let txt = el.textContent;
			if (
				// No swatch for JavaScript Math.tan
				last === "Math" ||
				// Ignore nested pl-c1 (see https://git.io/fNF3N)
				el.parentNode && el.parentNode.classList.contains("pl-c1")
			) {
				// noop
			} else if (!el.querySelector(".ghcc-block")) {
				if (el.classList.contains("pl-s")) {
					txt = txt.replace(regex.quotes, "");
				}
				if (formats.hex.regex.test(txt) || formats.named.regex.test(txt)) {
					addNode(el, txt.replace(regex.unix, "#"));
				} else if (formats.rgb.regex.test(txt)) {
					els = formats.rgb.find(els, el, txt);
				} else if (formats.hsl.regex.test(txt)) {
					els = formats.hsl.find(els, el, txt);
				}
			}
			last = txt;
			yield els;
		}
	}

	function addColors() {
		if (document.querySelector(".highlight")) {
			let status;
			// .pl-c1 targets css hex colors, "rgb" and "hsl"
			const els = [...document.querySelectorAll(".pl-c1, .pl-s, .pl-en, .pl-pds")];
			const iter = addBlock(els);
			const loop = () => {
				for (let i = 0; i < 40; i++) {
					status = iter.next();
				}
				if (!status.done) {
					requestAnimationFrame(loop);
				}
			};
			loop();
		}
	}

	document.addEventListener("ghmo:container", addColors);
	document.addEventListener("ghmo:preview", addColors);
	document.addEventListener("click", checkPopup);
	document.addEventListener("keyup", checkPopup);
	addColors();

})();