// ==UserScript==
// @name GitHub Code Language Icons
// @description Replaces GitHub's code language icons with Material Design Icons.
// @icon https://github.githubassets.com/favicons/favicon-dark.svg
// @version 1.0.9
// @author afkarxyz
// @namespace https://github.com/afkarxyz/misc-scripts/
// @supportURL https://github.com/afkarxyz/misc-scripts/issues
// @license MIT
// @match https://github.com/*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/';
const ICON_BASE_URL = `${BASE_URL}icons/`;
function normalizeLanguageName(language) {
const languageMappings = {
'cpp': ['c++'],
'csharp': ['c#'],
'sass': ['scss'],
'fsharp': ['f#'],
'qsharp': ['q#'],
'r': ['rmarkdown'],
'document': ['text'],
'bazel': ['starlark'],
'database': ['plpgsql'],
'docker': ['dockerfile'],
'shader': ['hlsl', 'glsl'],
'coffee': ['coffeescript'],
'kaitai': ['kaitai struct'],
'godot-assets': ['gdscript'],
'readme': ['restructuredtext'],
'jupyter': ['jupyter notebook'],
'console': ['batchfile', 'shell'],
'vim': ['vim script', 'vim snippet'],
'visualstudio': ['visual basic .net'],
'lisp': ['common lisp', 'emacs lisp'],
};
const normalizedLanguage = language.toLowerCase();
for (const [iconName, languageList] of Object.entries(languageMappings)) {
if (languageList.includes(normalizedLanguage)) {
return iconName;
}
}
return normalizedLanguage;
}
async function fetchAvailableIcons() {
const cacheKey = 'githubLanguageIconsCache';
const currentTime = Date.now();
const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
return cachedData.fileTypes;
}
try {
const response = await fetch(`${BASE_URL}icons.json`);
const data = await response.json();
GM_setValue(cacheKey, JSON.stringify({
fileTypes: data.fileTypes,
timestamp: currentTime
}));
return data.fileTypes;
} catch (error) {
console.error('Failed to fetch icon list:', error);
return cachedData.fileTypes || [];
}
}
async function replaceLanguageIcons() {
let availableIcons;
try {
availableIcons = await fetchAvailableIcons();
} catch (error) {
console.error('Error getting available icons:', error);
return;
}
const processedElements = new Set();
function processElement(element) {
if (processedElements.has(element)) return;
processedElements.add(element);
let langElement, language;
if (element.matches('.d-inline')) {
langElement = element.querySelector('.text-bold');
if (!langElement || langElement.textContent.toLowerCase() === 'other' || element.dataset.iconChecked) return;
language = normalizeLanguageName(langElement.textContent);
element.dataset.iconChecked = 'true';
const svg = element.querySelector('svg');
if (!svg || !availableIcons.includes(language)) return;
const img = document.createElement('img');
img.src = `${ICON_BASE_URL}${language}.svg`;
img.width = 16;
img.height = 16;
img.className = 'mr-2';
img.style.verticalAlign = 'middle';
svg.parentNode.replaceChild(img, svg);
}
else if (element.matches('.f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"]')) {
language = normalizeLanguageName(element.textContent);
if (!availableIcons.includes(language)) return;
const parentSpan = element.parentElement;
const colorSpan = parentSpan.querySelector('.repo-language-color');
const img = document.createElement('img');
img.src = `${ICON_BASE_URL}${language}.svg`;
img.width = 16;
img.height = 16;
img.style.marginRight = '2px';
img.style.verticalAlign = 'sub';
if (colorSpan) {
colorSpan.parentNode.insertBefore(img, colorSpan);
colorSpan.remove();
}
}
else if (element.matches('.mb-3 .no-wrap span[itemprop="programmingLanguage"]')) {
language = normalizeLanguageName(element.textContent);
if (!availableIcons.includes(language)) return;
const parentSpan = element.parentElement;
const colorSpan = parentSpan.querySelector('.repo-language-color');
const img = document.createElement('img');
img.src = `${ICON_BASE_URL}${language}.svg`;
img.width = 16;
img.height = 16;
img.style.marginRight = '4px';
const flexContainer = document.createElement('span');
flexContainer.style.display = 'inline-flex';
flexContainer.style.alignItems = 'center';
if (colorSpan) {
colorSpan.remove();
flexContainer.appendChild(img);
flexContainer.appendChild(element.cloneNode(true));
parentSpan.replaceWith(flexContainer);
}
}
else if (element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
const preLanguageElement = element.querySelector('.Box-sc-g0xbh4-0:not(.fVplbS)');
if (preLanguageElement) {
preLanguageElement.style.display = 'none';
}
const languageSpan = element.querySelector('.Box-sc-g0xbh4-0.fVplbS');
if (languageSpan && !languageSpan.dataset.iconProcessed) {
language = normalizeLanguageName(languageSpan.textContent.trim());
if (availableIcons.includes(language)) {
const iconImg = document.createElement('img');
iconImg.src = `${ICON_BASE_URL}${language}.svg`;
iconImg.alt = `${language} icon`;
iconImg.width = 16;
iconImg.height = 16;
iconImg.style.marginRight = '2px';
iconImg.style.verticalAlign = 'middle';
languageSpan.insertAdjacentElement('beforebegin', iconImg);
languageSpan.dataset.iconProcessed = 'true';
}
}
}
else if (element.matches('.repo-language-color')) {
const languageSpan = element.parentElement.querySelector('[itemprop="programmingLanguage"]');
if (languageSpan && !languageSpan.dataset.iconProcessed) {
language = normalizeLanguageName(languageSpan.textContent);
if (availableIcons.includes(language)) {
const iconImg = document.createElement('img');
iconImg.src = `${ICON_BASE_URL}${language}.svg`;
iconImg.alt = `${language} icon`;
iconImg.width = 16;
iconImg.height = 16;
iconImg.style.marginRight = '2px';
iconImg.style.verticalAlign = 'sub';
element.parentElement.insertBefore(iconImg, element);
element.remove();
languageSpan.dataset.iconProcessed = 'true';
}
}
}
else if (element.matches('.Box-sc-g0xbh4-0.hjDqIa')) {
const languageSpan = element.nextElementSibling;
if (languageSpan && languageSpan.getAttribute('aria-label') && !languageSpan.dataset.iconProcessed) {
language = normalizeLanguageName(languageSpan.getAttribute('aria-label').replace(' language', ''));
if (availableIcons.includes(language)) {
const iconImg = document.createElement('img');
iconImg.src = `${ICON_BASE_URL}${language}.svg`;
iconImg.alt = `${language} icon`;
iconImg.width = 16;
iconImg.height = 16;
iconImg.style.marginRight = '4px';
iconImg.style.verticalAlign = 'middle';
element.style.display = 'none';
languageSpan.parentNode.insertBefore(iconImg, languageSpan);
languageSpan.dataset.iconProcessed = 'true';
}
}
}
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
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')) {
processElement(node);
} else {
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);
}
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
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);
}
function hideAndReplaceDivs() {
document.querySelectorAll('span.Box-sc-g0xbh4-0.eXDtUe').forEach(spanElement => {
if (!spanElement) return;
const divToHide = spanElement.querySelector('div[class^="Box-sc-g0xbh4-0"]');
const titleElement = spanElement.closest('a')?.querySelector('div[title]');
const title = titleElement?.getAttribute('title');
if (divToHide && title && !divToHide.dataset.processed) {
const normalizedTitle = normalizeLanguageName(title);
divToHide.style.display = 'none';
divToHide.dataset.processed = 'true';
const svgElement = document.createElement('img');
svgElement.src = `${ICON_BASE_URL}${normalizedTitle}.svg`;
svgElement.alt = title;
svgElement.width = svgElement.height = 16;
divToHide.parentNode.insertBefore(svgElement, divToHide);
}
});
}
function observeDOMChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
hideAndReplaceDivs();
}
});
});
const config = { childList: true, subtree: true };
observer.observe(document.body, config);
hideAndReplaceDivs();
}
function init() {
replaceLanguageIcons();
observeDOMChanges();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();