GitHub Code Colors

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

目前为 2022-10-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Colors
  3. // @version 2.0.8
  4. // @description A userscript that adds a color swatch next to the code color definition
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://*.github.com/*
  9. // @run-at document-idle
  10. // @grant GM.addStyle
  11. // @grant GM_addStyle
  12. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
  13. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
  14. // @require https://greasyfork.org/scripts/387811-color-bundle/code/color-bundle.js?version=719499
  15. // @icon https://github.githubassets.com/pinned-octocat.svg
  16. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  17. // ==/UserScript==
  18. /* global Color */
  19. (() => {
  20. "use strict";
  21.  
  22. // whitespace:initial => overrides code-wrap css in content
  23. GM.addStyle(`
  24. .ghcc-block { width:14px; height:14px; display:inline-block;
  25. vertical-align:middle; margin-right:4px; border-radius:4px;
  26. border:1px solid rgba(119, 119, 119, 0.5); position:relative;
  27. background-image:none; cursor:pointer; }
  28. .ghcc-popup { position:absolute; background:#222; color:#eee;
  29. min-width:350px; top:100%; left:0px; padding:10px; z-index:100;
  30. white-space:pre; cursor:text; text-align:left; -webkit-user-select:text;
  31. -moz-user-select:text; -ms-user-select:text; user-select:text; }
  32. .markdown-body .highlight pre, .markdown-body pre {
  33. overflow-y:visible !important; }
  34. .ghcc-copy { padding:2px 6px; margin-right:4px; background:transparent;
  35. border:0; }`);
  36.  
  37. const namedColors = Object.keys(Color.namedColors);
  38. const namedColorsList = namedColors.reduce((acc, name) => {
  39. acc[name] = `rgb(${Color.namedColors[name].join(", ")})`;
  40. return acc;
  41. }, {});
  42.  
  43. const copyButton = document.createElement("clipboard-copy");
  44. copyButton.className = "btn btn-sm btn-blue tooltipped tooltipped-w ghcc-copy";
  45. copyButton.setAttribute("aria-label", "Copy to clipboard");
  46. // This hint isn't working yet (GitHub needs to fix it)
  47. copyButton.setAttribute("data-copied-hint", "Copied!");
  48. copyButton.innerHTML = `
  49. <svg aria-hidden="true" class="octicon octicon-clippy" height="14" viewBox="0 0 14 16" width="14">
  50. <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>
  51. </svg>`;
  52.  
  53. // Misc regex
  54. const regex = {
  55. quotes: /['"]/g,
  56. unix: /^0x/,
  57. percent: /%%/g
  58. };
  59.  
  60. // Don't use a div, because GitHub-Dark adds a :hover background
  61. // color definition on divs
  62. const block = document.createElement("button");
  63. block.className = "ghcc-block";
  64. block.tabIndex = 0;
  65. // prevent submitting on click in comment preview
  66. block.type = "button";
  67. block.onclick = "event => event.stopPropagation()";
  68.  
  69. const br = document.createElement("br");
  70.  
  71. const popup = document.createElement("span");
  72. popup.className = "ghcc-popup";
  73.  
  74. const formats = {
  75. named: {
  76. regex: new RegExp("^(" + namedColors.join("|") + ")$", "i"),
  77. convert: color => {
  78. const rgb = color.rgb().toString();
  79. if (Object.values(namedColorsList).includes(rgb)) {
  80. // There may be more than one named color
  81. // e.g. "slategray" & "slategrey"
  82. return Object.keys(namedColorsList)
  83. .filter(n => namedColorsList[n] === rgb)
  84. .join("<br />");
  85. }
  86. return "";
  87. },
  88. },
  89. hex: {
  90. // Ex: #123, #123456 or 0x123456 (unix style colors, used by three.js)
  91. regex: /^(#|0x)([0-9A-F]{6,8}|[0-9A-F]{3,4})$/i,
  92. convert: color => `${color.hex().toString()}`,
  93. },
  94. rgb: {
  95. regex: /^rgba?(\([^\)]+\))?/i,
  96. regexAlpha: /rgba/i,
  97. find: (els, el, txt) => {
  98. // Color in a string contains everything
  99. if (el.classList.contains("pl-s")) {
  100. txt = txt.match(formats.rgb.regex)[0];
  101. } else {
  102. // Rgb(a) colors contained in multiple "pl-c1" spans
  103. let indx = formats.rgb.regexAlpha.test(txt) ? 4 : 3;
  104. const tmp = [];
  105. while (indx) {
  106. tmp.push(getTextContent(els.shift()));
  107. indx--;
  108. }
  109. txt += "(" + tmp.join(",") + ")";
  110. }
  111. addNode(el, txt);
  112. return els;
  113. },
  114. convert: color => {
  115. const rgb = color.rgb().alpha(1).toString();
  116. const rgba = color.rgb().toString();
  117. return `${rgb}${rgb === rgba ? "" : "; " + rgba}`;
  118. }
  119. },
  120. hsl: {
  121. // Ex: hsl(0,0%,0%) or hsla(0,0%,0%,0.2);
  122. regex: /^hsla?(\([^\)]+\))?/i,
  123. find: (els, el, txt) => {
  124. const tmp = /a$/i.test(txt);
  125. if (el.classList.contains("pl-s")) {
  126. // Color in a string contains everything
  127. txt = txt.match(formats.hsl.regex)[0];
  128. } else {
  129. // Traverse this HTML... & els only contains the pl-c1 nodes
  130. // <span class="pl-c1">hsl</span>(<span class="pl-c1">1</span>,
  131. // <span class="pl-c1">1</span><span class="pl-k">%</span>,
  132. // <span class="pl-c1">1</span><span class="pl-k">%</span>);
  133. // using getTextContent in case of invalid css
  134. txt = txt + "(" + getTextContent(els.shift()) + "," +
  135. getTextContent(els.shift()) + "%," +
  136. // Hsla needs one more parameter
  137. getTextContent(els.shift()) + "%" +
  138. (tmp ? "," + getTextContent(els.shift()) : "") + ")";
  139. }
  140. // Sometimes (previews only?) the .pl-k span is nested inside
  141. // the .pl-c1 span, so we end up with "%%"
  142. addNode(el, txt.replace(regex.percent, "%"));
  143. return els;
  144. },
  145. convert: color => {
  146. const hsl = color.hsl().alpha(1).round().toString();
  147. const hsla = color.hsl().round().toString();
  148. return `${hsl}${hsl === hsla ? "" : "; " + hsla}`;
  149. }
  150. },
  151. hwb: {
  152. convert: color => color.hwb().round().toString()
  153. },
  154. cymk: {
  155. convert: color => {
  156. const cmyk = color.cmyk().round().array(); // array of numbers
  157. return `device-cmyk(${cmyk.shift()}, ${cmyk.join("%, ")})`;
  158. }
  159. },
  160. };
  161.  
  162. function showPopup(el) {
  163. const popup = createPopup(el.style.backgroundColor);
  164. el.appendChild(popup);
  165. }
  166.  
  167. function hidePopup(el) {
  168. el.textContent = "";
  169. }
  170.  
  171. function checkPopup(event) {
  172. const el = event.target;
  173. if (el && el.classList.contains("ghcc-block")) {
  174. if (event.type === "click") {
  175. if (el.textContent) {
  176. hidePopup(el)
  177. } else {
  178. showPopup(el);
  179. }
  180. }
  181. }
  182. if (event.type === "keyup" && event.key === "Escape") {
  183. // hide all popups
  184. [...document.querySelectorAll(".ghcc-block")].forEach(el => {
  185. el.textContent = "";
  186. });
  187. }
  188. }
  189.  
  190. function createPopup(val) {
  191. const color = Color(val);
  192. const el = popup.cloneNode();
  193. const fragment = document.createDocumentFragment();
  194. Object.keys(formats).forEach(type => {
  195. if (typeof formats[type].convert === "function") {
  196. const val = formats[type].convert(color);
  197. if (val) {
  198. const button = copyButton.cloneNode(true);
  199. button.value = val;
  200. fragment.appendChild(button);
  201. fragment.appendChild(document.createTextNode(val));
  202. fragment.appendChild(br.cloneNode());
  203. }
  204. }
  205. });
  206. el.appendChild(fragment);
  207. return el;
  208. }
  209.  
  210. function addNode(el, val) {
  211. const node = block.cloneNode();
  212. node.style.backgroundColor = val;
  213. // Don't add node if color is invalid
  214. if (node.style.backgroundColor !== "") {
  215. el.insertBefore(node, el.childNodes[0]);
  216. }
  217. }
  218.  
  219. function getTextContent(el) {
  220. return el ? el.textContent : "";
  221. }
  222.  
  223. // Loop with delay to allow user interaction
  224. function* addBlock(els) {
  225. let last = "";
  226. while (els.length) {
  227. let el = els.shift();
  228. let txt = el.textContent;
  229. if (
  230. // No swatch for JavaScript Math.tan
  231. last === "Math" ||
  232. // Ignore nested pl-c1 (see https://git.io/fNF3N)
  233. el.parentNode && el.parentNode.classList.contains("pl-c1")
  234. ) {
  235. // noop
  236. } else if (!el.querySelector(".ghcc-block")) {
  237. if (el.classList.contains("pl-s")) {
  238. txt = txt.replace(regex.quotes, "");
  239. }
  240. if (formats.hex.regex.test(txt) || formats.named.regex.test(txt)) {
  241. addNode(el, txt.replace(regex.unix, "#"));
  242. } else if (formats.rgb.regex.test(txt)) {
  243. els = formats.rgb.find(els, el, txt);
  244. } else if (formats.hsl.regex.test(txt)) {
  245. els = formats.hsl.find(els, el, txt);
  246. }
  247. }
  248. last = txt;
  249. yield els;
  250. }
  251. }
  252.  
  253. function addColors() {
  254. if (document.querySelector(".highlight")) {
  255. let status;
  256. // .pl-c1 targets css hex colors, "rgb" and "hsl"
  257. const els = [...document.querySelectorAll(".pl-c1, .pl-s, .pl-en, .pl-pds")];
  258. const iter = addBlock(els);
  259. const loop = () => {
  260. for (let i = 0; i < 40; i++) {
  261. status = iter.next();
  262. }
  263. if (!status.done) {
  264. requestAnimationFrame(loop);
  265. }
  266. };
  267. loop();
  268. }
  269. }
  270.  
  271. document.addEventListener("ghmo:container", addColors);
  272. document.addEventListener("ghmo:preview", addColors);
  273. document.addEventListener("click", checkPopup);
  274. document.addEventListener("keyup", checkPopup);
  275. addColors();
  276.  
  277. })();