GitHub Code Language Icons

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

当前为 2025-01-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Language Icons
  3. // @description Replaces GitHub's boring round code language icons with Material Design Icons.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.6
  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. async function fetchLanguageMappings() {
  22. const cacheKey = 'githubLanguageRemapCache';
  23. const currentTime = Date.now();
  24. const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
  25. if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
  26. return cachedData.mappings;
  27. }
  28. try {
  29. const response = await fetch(`${BASE_URL}remap.json`);
  30. const data = await response.json();
  31. GM_setValue(cacheKey, JSON.stringify({
  32. mappings: data.iconRemap,
  33. timestamp: currentTime
  34. }));
  35. return data.iconRemap;
  36. } catch (error) {
  37. console.error('Failed to fetch language mappings:', error);
  38. return cachedData.mappings || {};
  39. }
  40. }
  41.  
  42. let languageMappings = {};
  43.  
  44. function normalizeLanguageName(language) {
  45. const normalizedLanguage = language.toLowerCase();
  46. for (const [iconName, languageList] of Object.entries(languageMappings)) {
  47. if (languageList.includes(normalizedLanguage)) {
  48. return iconName;
  49. }
  50. }
  51. return normalizedLanguage;
  52. }
  53.  
  54. async function fetchAvailableIcons() {
  55. const cacheKey = 'githubLanguageIconsCache';
  56. const currentTime = Date.now();
  57. const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
  58.  
  59. if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
  60. return cachedData.fileTypes;
  61. }
  62.  
  63. try {
  64. const response = await fetch(`${BASE_URL}icons.json`);
  65. const data = await response.json();
  66.  
  67. GM_setValue(cacheKey, JSON.stringify({
  68. fileTypes: data.fileTypes,
  69. timestamp: currentTime
  70. }));
  71.  
  72. return data.fileTypes;
  73. } catch (error) {
  74. console.error('Failed to fetch icon list:', error);
  75. return cachedData.fileTypes || [];
  76. }
  77. }
  78.  
  79. async function replaceLanguageIcons() {
  80. let availableIcons;
  81. try {
  82. availableIcons = await fetchAvailableIcons();
  83. } catch (error) {
  84. console.error('Error getting available icons:', error);
  85. return;
  86. }
  87. const processedElements = new Set();
  88.  
  89. async function replaceOtherIcon(targetSvg) {
  90. try {
  91. const response = await fetch(`${ICON_BASE_URL}other.svg`);
  92. const svgText = await response.text();
  93. const parser = new DOMParser();
  94. const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
  95. const newSvg = svgDoc.querySelector('svg');
  96. const attributes = targetSvg.attributes;
  97. for (let i = 0; i < attributes.length; i++) {
  98. const attr = attributes[i];
  99. if (attr.name !== 'class') {
  100. newSvg.setAttribute(attr.name, attr.value);
  101. }
  102. }
  103. newSvg.setAttribute('width', '16');
  104. newSvg.setAttribute('height', '16');
  105. newSvg.setAttribute('viewBox', '0 0 24 24');
  106. const innerGroup = newSvg.querySelector('g') || newSvg.querySelector('path');
  107. if (innerGroup) {
  108. innerGroup.setAttribute('transform', 'scale(0.67)');
  109. }
  110. const targetClasses = Array.from(targetSvg.classList);
  111. targetClasses.forEach(className => {
  112. if (className !== 'mr-2') {
  113. newSvg.classList.add(className);
  114. }
  115. });
  116. newSvg.classList.add('mr-1');
  117. newSvg.style.width = '16px';
  118. newSvg.style.height = '16px';
  119. newSvg.style.minWidth = '16px';
  120. newSvg.style.minHeight = '16px';
  121. targetSvg.parentNode.replaceChild(newSvg, targetSvg);
  122. } catch (error) {
  123. console.error('Failed to replace Other icon:', error);
  124. }
  125. }
  126.  
  127. function processElement(element) {
  128. if (processedElements.has(element)) return;
  129. processedElements.add(element);
  130.  
  131. let langElement, language;
  132. // Case 1: Action List Items
  133. if (element.matches('.ActionListItem-visual.ActionListItem-visual--leading')) {
  134. const languageLabel = element.closest('.ActionListContent')?.querySelector('.ActionListItem-label.text-normal');
  135. if (!languageLabel || !languageLabel.textContent || element.dataset.iconChecked) return;
  136. language = normalizeLanguageName(languageLabel.textContent);
  137. element.dataset.iconChecked = 'true';
  138. const colorDiv = element.querySelector('div');
  139. if (!colorDiv || !availableIcons.includes(language)) return;
  140. const img = document.createElement('img');
  141. img.src = `${ICON_BASE_URL}${language}.svg`;
  142. img.width = 16;
  143. img.height = 16;
  144. img.style.verticalAlign = 'middle';
  145. colorDiv.replaceWith(img);
  146. }
  147. // Case 2: Inline Displays
  148. else if (element.matches('.d-inline')) {
  149. langElement = element.querySelector('.text-bold');
  150. if (!langElement || langElement.textContent.toLowerCase() === 'other' || element.dataset.iconChecked) return;
  151. language = normalizeLanguageName(langElement.textContent);
  152. element.dataset.iconChecked = 'true';
  153. const svg = element.querySelector('svg');
  154.  
  155. if (!svg || !availableIcons.includes(language)) return;
  156. const img = document.createElement('img');
  157. img.src = `${ICON_BASE_URL}${language}.svg`;
  158. img.width = 16;
  159. img.height = 16;
  160. img.className = 'mr-2';
  161. img.style.verticalAlign = 'middle';
  162. svg.parentNode.replaceChild(img, svg);
  163. }
  164. // Case 3: Repository Language Indicators
  165. else if (element.matches('.f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"]')) {
  166. language = normalizeLanguageName(element.textContent);
  167. if (!availableIcons.includes(language)) return;
  168. const parentSpan = element.parentElement;
  169. const colorSpan = parentSpan.querySelector('.repo-language-color');
  170. const img = document.createElement('img');
  171. img.src = `${ICON_BASE_URL}${language}.svg`;
  172. img.width = 16;
  173. img.height = 16;
  174. img.style.marginRight = '2px';
  175. img.style.verticalAlign = 'sub';
  176. if (colorSpan) {
  177. colorSpan.parentNode.insertBefore(img, colorSpan);
  178. colorSpan.remove();
  179. }
  180. }
  181. // Case 4: No Wrap Language Spans
  182. else if (element.matches('.mb-3 .no-wrap span[itemprop="programmingLanguage"]')) {
  183. language = normalizeLanguageName(element.textContent);
  184. if (!availableIcons.includes(language)) return;
  185. const parentSpan = element.parentElement;
  186. const colorSpan = parentSpan.querySelector('.repo-language-color');
  187. const img = document.createElement('img');
  188. img.src = `${ICON_BASE_URL}${language}.svg`;
  189. img.width = 16;
  190. img.height = 16;
  191. img.style.marginRight = '4px';
  192. const flexContainer = document.createElement('span');
  193. flexContainer.style.display = 'inline-flex';
  194. flexContainer.style.alignItems = 'center';
  195. if (colorSpan) {
  196. colorSpan.remove();
  197. flexContainer.appendChild(img);
  198. flexContainer.appendChild(element.cloneNode(true));
  199. parentSpan.replaceWith(flexContainer);
  200. }
  201. }
  202. // Case 5: File Type Boxes
  203. else if (element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
  204. const preElements = element.querySelectorAll('.Box-sc-g0xbh4-0');
  205. preElements.forEach(preElement => {
  206. preElement.style.display = 'none';
  207. });
  208.  
  209. const languageSpan = element.querySelector('.Text__StyledText-sc-17v1xeu-0');
  210. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  211. language = normalizeLanguageName(languageSpan.textContent.trim());
  212. if (availableIcons.includes(language)) {
  213. const iconImg = document.createElement('img');
  214. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  215. iconImg.alt = `${language} icon`;
  216. iconImg.width = 16;
  217. iconImg.height = 16;
  218. iconImg.style.marginRight = '2px';
  219. iconImg.style.verticalAlign = 'middle';
  220.  
  221. languageSpan.insertAdjacentElement('beforebegin', iconImg);
  222. languageSpan.dataset.iconProcessed = 'true';
  223. }
  224. }
  225. }
  226. // Case 6: Repository Language Colors
  227. else if (element.matches('.repo-language-color')) {
  228. const languageSpan = element.parentElement.querySelector('[itemprop="programmingLanguage"]');
  229. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  230. language = normalizeLanguageName(languageSpan.textContent);
  231. if (availableIcons.includes(language)) {
  232. const iconImg = document.createElement('img');
  233. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  234. iconImg.alt = `${language} icon`;
  235. iconImg.width = 16;
  236. iconImg.height = 16;
  237. iconImg.style.marginRight = '2px';
  238. iconImg.style.verticalAlign = 'sub';
  239.  
  240. element.parentElement.insertBefore(iconImg, element);
  241. element.remove();
  242. languageSpan.dataset.iconProcessed = 'true';
  243. }
  244. }
  245. }
  246. // Case 7: Special File Type Boxes
  247. else if (element.matches('.Box-sc-g0xbh4-0.hjDqIa')) {
  248. const languageSpan = element.nextElementSibling;
  249. if (languageSpan && languageSpan.getAttribute('aria-label') && !languageSpan.dataset.iconProcessed) {
  250. language = normalizeLanguageName(languageSpan.getAttribute('aria-label').replace(' language', ''));
  251. if (availableIcons.includes(language)) {
  252. const iconImg = document.createElement('img');
  253. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  254. iconImg.alt = `${language} icon`;
  255. iconImg.width = 16;
  256. iconImg.height = 16;
  257. iconImg.style.marginRight = '4px';
  258. iconImg.style.verticalAlign = 'middle';
  259.  
  260. element.style.display = 'none';
  261. languageSpan.parentNode.insertBefore(iconImg, languageSpan);
  262. languageSpan.dataset.iconProcessed = 'true';
  263. }
  264. }
  265. }
  266. // Case 8: Other Language Icons
  267. else if (element.matches('.octicon-dot-fill')) {
  268. const parentElement = element.closest('.d-inline');
  269. if (parentElement) {
  270. const languageElement = parentElement.querySelector('.text-bold');
  271. if (languageElement && languageElement.textContent.toLowerCase() === 'other' && !element.dataset.iconProcessed) {
  272. element.dataset.iconProcessed = 'true';
  273. replaceOtherIcon(element);
  274. }
  275. }
  276. }
  277. }
  278.  
  279. const observer = new MutationObserver((mutations) => {
  280. for (const mutation of mutations) {
  281. for (const node of mutation.addedNodes) {
  282. if (node.nodeType === Node.ELEMENT_NODE) {
  283. 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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill')) {
  284. processElement(node);
  285. } else {
  286. 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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').forEach(processElement);
  287. }
  288. }
  289. }
  290. }
  291. });
  292.  
  293. observer.observe(document.body, {
  294. childList: true,
  295. subtree: true
  296. });
  297.  
  298. 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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').forEach(processElement);
  299. }
  300.  
  301. // Case 9: File Browser Language Boxes
  302. function hideAndReplaceDivs() {
  303. document.querySelectorAll('span.Box-sc-g0xbh4-0.lnwIhU').forEach(spanElement => {
  304. if (!spanElement) return;
  305.  
  306. const divToHide = spanElement.querySelector('div[class^="Box-sc-g0xbh4-0"]');
  307. const titleElement = spanElement.closest('a')?.querySelector('div[title]');
  308. const title = titleElement?.getAttribute('title');
  309. if (divToHide && title && !divToHide.dataset.processed) {
  310. const normalizedTitle = normalizeLanguageName(title);
  311. divToHide.style.display = 'none';
  312. divToHide.dataset.processed = 'true';
  313. const svgElement = document.createElement('img');
  314. svgElement.src = `${ICON_BASE_URL}${normalizedTitle}.svg`;
  315. svgElement.alt = title;
  316. svgElement.width = svgElement.height = 16;
  317. divToHide.parentNode.insertBefore(svgElement, divToHide);
  318. }
  319. });
  320. }
  321.  
  322. function observeDOMChanges() {
  323. const observer = new MutationObserver((mutations) => {
  324. mutations.forEach((mutation) => {
  325. if (mutation.type === 'childList') {
  326. hideAndReplaceDivs();
  327. }
  328. });
  329. });
  330.  
  331. const config = { childList: true, subtree: true };
  332. observer.observe(document.body, config);
  333. hideAndReplaceDivs();
  334. }
  335.  
  336. async function init() {
  337. languageMappings = await fetchLanguageMappings();
  338. replaceLanguageIcons();
  339. observeDOMChanges();
  340. }
  341.  
  342. if (document.readyState === 'loading') {
  343. document.addEventListener('DOMContentLoaded', init);
  344. } else {
  345. init();
  346. }
  347. })();