// ==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 // @updateURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-code-colors.user.js // @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-code-colors.user.js // @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 = ` `; // 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("
"); } 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 // hsl(1, // 1%, // 1%); // 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(); })();