GitHub Code Colors

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

目前为 2019-07-24 提交的版本。查看 最新版本

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