GitHub Code Colors

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

Versión del día 24/10/2022. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        GitHub Code Colors
// @version     2.0.8
// @description A userscript that adds a color swatch next to the code color definition
// @license     MIT
// @author      Rob Garrison
// @namespace   https://github.com/Mottie
// @include     https://*.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();

})();