GitHub Code Language Icons

Replaces GitHub's code language icons with Material Design Icons.

当前为 2024-12-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Language Icons
  3. // @description Replaces GitHub's code language icons with Material Design Icons.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.0.9
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/misc-scripts/
  8. // @supportURL https://github.com/afkarxyz/misc-scripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/';
  19. const ICON_BASE_URL = `${BASE_URL}icons/`;
  20.  
  21. function normalizeLanguageName(language) {
  22. const languageMappings = {
  23. 'cpp': ['c++'],
  24. 'csharp': ['c#'],
  25. 'sass': ['scss'],
  26. 'fsharp': ['f#'],
  27. 'qsharp': ['q#'],
  28. 'r': ['rmarkdown'],
  29. 'document': ['text'],
  30. 'bazel': ['starlark'],
  31. 'database': ['plpgsql'],
  32. 'docker': ['dockerfile'],
  33. 'shader': ['hlsl', 'glsl'],
  34. 'coffee': ['coffeescript'],
  35. 'kaitai': ['kaitai struct'],
  36. 'godot-assets': ['gdscript'],
  37. 'readme': ['restructuredtext'],
  38. 'jupyter': ['jupyter notebook'],
  39. 'console': ['batchfile', 'shell'],
  40. 'vim': ['vim script', 'vim snippet'],
  41. 'visualstudio': ['visual basic .net'],
  42. 'lisp': ['common lisp', 'emacs lisp'],
  43. };
  44.  
  45. const normalizedLanguage = language.toLowerCase();
  46.  
  47. for (const [iconName, languageList] of Object.entries(languageMappings)) {
  48. if (languageList.includes(normalizedLanguage)) {
  49. return iconName;
  50. }
  51. }
  52.  
  53. return normalizedLanguage;
  54. }
  55.  
  56. async function fetchAvailableIcons() {
  57. const cacheKey = 'githubLanguageIconsCache';
  58. const currentTime = Date.now();
  59.  
  60. const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
  61.  
  62. if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
  63. return cachedData.fileTypes;
  64. }
  65.  
  66. try {
  67. const response = await fetch(`${BASE_URL}icons.json`);
  68. const data = await response.json();
  69.  
  70. GM_setValue(cacheKey, JSON.stringify({
  71. fileTypes: data.fileTypes,
  72. timestamp: currentTime
  73. }));
  74.  
  75. return data.fileTypes;
  76. } catch (error) {
  77. console.error('Failed to fetch icon list:', error);
  78. return cachedData.fileTypes || [];
  79. }
  80. }
  81.  
  82. async function replaceLanguageIcons() {
  83. let availableIcons;
  84. try {
  85. availableIcons = await fetchAvailableIcons();
  86. } catch (error) {
  87. console.error('Error getting available icons:', error);
  88. return;
  89. }
  90. const processedElements = new Set();
  91.  
  92. function processElement(element) {
  93. if (processedElements.has(element)) return;
  94. processedElements.add(element);
  95.  
  96. let langElement, language;
  97. if (element.matches('.d-inline')) {
  98. langElement = element.querySelector('.text-bold');
  99. if (!langElement || langElement.textContent.toLowerCase() === 'other' || element.dataset.iconChecked) return;
  100. language = normalizeLanguageName(langElement.textContent);
  101. element.dataset.iconChecked = 'true';
  102. const svg = element.querySelector('svg');
  103.  
  104. if (!svg || !availableIcons.includes(language)) return;
  105. const img = document.createElement('img');
  106. img.src = `${ICON_BASE_URL}${language}.svg`;
  107. img.width = 16;
  108. img.height = 16;
  109. img.className = 'mr-2';
  110. img.style.verticalAlign = 'middle';
  111. svg.parentNode.replaceChild(img, svg);
  112. }
  113. else if (element.matches('.f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"]')) {
  114. language = normalizeLanguageName(element.textContent);
  115. if (!availableIcons.includes(language)) return;
  116. const parentSpan = element.parentElement;
  117. const colorSpan = parentSpan.querySelector('.repo-language-color');
  118. const img = document.createElement('img');
  119. img.src = `${ICON_BASE_URL}${language}.svg`;
  120. img.width = 16;
  121. img.height = 16;
  122. img.style.marginRight = '2px';
  123. img.style.verticalAlign = 'sub';
  124. if (colorSpan) {
  125. colorSpan.parentNode.insertBefore(img, colorSpan);
  126. colorSpan.remove();
  127. }
  128. }
  129. else if (element.matches('.mb-3 .no-wrap span[itemprop="programmingLanguage"]')) {
  130. language = normalizeLanguageName(element.textContent);
  131. if (!availableIcons.includes(language)) return;
  132. const parentSpan = element.parentElement;
  133. const colorSpan = parentSpan.querySelector('.repo-language-color');
  134. const img = document.createElement('img');
  135. img.src = `${ICON_BASE_URL}${language}.svg`;
  136. img.width = 16;
  137. img.height = 16;
  138. img.style.marginRight = '4px';
  139. const flexContainer = document.createElement('span');
  140. flexContainer.style.display = 'inline-flex';
  141. flexContainer.style.alignItems = 'center';
  142. if (colorSpan) {
  143. colorSpan.remove();
  144. flexContainer.appendChild(img);
  145. flexContainer.appendChild(element.cloneNode(true));
  146. parentSpan.replaceWith(flexContainer);
  147. }
  148. }
  149. else if (element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
  150. const preLanguageElement = element.querySelector('.Box-sc-g0xbh4-0:not(.fVplbS)');
  151. if (preLanguageElement) {
  152. preLanguageElement.style.display = 'none';
  153. }
  154.  
  155. const languageSpan = element.querySelector('.Box-sc-g0xbh4-0.fVplbS');
  156. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  157. language = normalizeLanguageName(languageSpan.textContent.trim());
  158. if (availableIcons.includes(language)) {
  159. const iconImg = document.createElement('img');
  160. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  161. iconImg.alt = `${language} icon`;
  162. iconImg.width = 16;
  163. iconImg.height = 16;
  164. iconImg.style.marginRight = '2px';
  165. iconImg.style.verticalAlign = 'middle';
  166.  
  167. languageSpan.insertAdjacentElement('beforebegin', iconImg);
  168. languageSpan.dataset.iconProcessed = 'true';
  169. }
  170. }
  171. }
  172. else if (element.matches('.repo-language-color')) {
  173. const languageSpan = element.parentElement.querySelector('[itemprop="programmingLanguage"]');
  174. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  175. language = normalizeLanguageName(languageSpan.textContent);
  176. if (availableIcons.includes(language)) {
  177. const iconImg = document.createElement('img');
  178. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  179. iconImg.alt = `${language} icon`;
  180. iconImg.width = 16;
  181. iconImg.height = 16;
  182. iconImg.style.marginRight = '2px';
  183. iconImg.style.verticalAlign = 'sub';
  184.  
  185. element.parentElement.insertBefore(iconImg, element);
  186. element.remove();
  187. languageSpan.dataset.iconProcessed = 'true';
  188. }
  189. }
  190. }
  191. else if (element.matches('.Box-sc-g0xbh4-0.hjDqIa')) {
  192. const languageSpan = element.nextElementSibling;
  193. if (languageSpan && languageSpan.getAttribute('aria-label') && !languageSpan.dataset.iconProcessed) {
  194. language = normalizeLanguageName(languageSpan.getAttribute('aria-label').replace(' language', ''));
  195. if (availableIcons.includes(language)) {
  196. const iconImg = document.createElement('img');
  197. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  198. iconImg.alt = `${language} icon`;
  199. iconImg.width = 16;
  200. iconImg.height = 16;
  201. iconImg.style.marginRight = '4px';
  202. iconImg.style.verticalAlign = 'middle';
  203.  
  204. element.style.display = 'none';
  205.  
  206. languageSpan.parentNode.insertBefore(iconImg, languageSpan);
  207. languageSpan.dataset.iconProcessed = 'true';
  208. }
  209. }
  210. }
  211. }
  212.  
  213. const observer = new MutationObserver((mutations) => {
  214. for (const mutation of mutations) {
  215. for (const node of mutation.addedNodes) {
  216. if (node.nodeType === Node.ELEMENT_NODE) {
  217. if (node.matches('.d-inline, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa')) {
  218. processElement(node);
  219. } else {
  220. node.querySelectorAll('.d-inline, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa').forEach(processElement);
  221. }
  222. }
  223. }
  224. }
  225. });
  226.  
  227. observer.observe(document.body, {
  228. childList: true,
  229. subtree: true
  230. });
  231.  
  232. document.querySelectorAll('.d-inline, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa').forEach(processElement);
  233. }
  234.  
  235. function hideAndReplaceDivs() {
  236. document.querySelectorAll('span.Box-sc-g0xbh4-0.eXDtUe').forEach(spanElement => {
  237. if (!spanElement) return;
  238.  
  239. const divToHide = spanElement.querySelector('div[class^="Box-sc-g0xbh4-0"]');
  240. const titleElement = spanElement.closest('a')?.querySelector('div[title]');
  241. const title = titleElement?.getAttribute('title');
  242. if (divToHide && title && !divToHide.dataset.processed) {
  243. const normalizedTitle = normalizeLanguageName(title);
  244. divToHide.style.display = 'none';
  245. divToHide.dataset.processed = 'true';
  246. const svgElement = document.createElement('img');
  247. svgElement.src = `${ICON_BASE_URL}${normalizedTitle}.svg`;
  248. svgElement.alt = title;
  249. svgElement.width = svgElement.height = 16;
  250. divToHide.parentNode.insertBefore(svgElement, divToHide);
  251. }
  252. });
  253. }
  254.  
  255. function observeDOMChanges() {
  256. const observer = new MutationObserver((mutations) => {
  257. mutations.forEach((mutation) => {
  258. if (mutation.type === 'childList') {
  259. hideAndReplaceDivs();
  260. }
  261. });
  262. });
  263.  
  264. const config = { childList: true, subtree: true };
  265. observer.observe(document.body, config);
  266.  
  267. hideAndReplaceDivs();
  268. }
  269.  
  270. function init() {
  271. replaceLanguageIcons();
  272. observeDOMChanges();
  273. }
  274.  
  275. if (document.readyState === 'loading') {
  276. document.addEventListener('DOMContentLoaded', init);
  277. } else {
  278. init();
  279. }
  280. })();