GitHub Code Language Icons

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

À data de 10/01/2025. Ver a versão mais recente.

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name         GitHub Code Language Icons
// @description  Replaces GitHub's boring round code language icons with Material Design Icons.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.6
// @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/`;

  async function fetchLanguageMappings() {
    const cacheKey = 'githubLanguageRemapCache';
    const currentTime = Date.now();
    const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
    
    if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
      return cachedData.mappings;
    }
    
    try {
      const response = await fetch(`${BASE_URL}remap.json`);
      const data = await response.json();
      
      GM_setValue(cacheKey, JSON.stringify({
        mappings: data.iconRemap,
        timestamp: currentTime
      }));
      
      return data.iconRemap;
    } catch (error) {
      console.error('Failed to fetch language mappings:', error);
      return cachedData.mappings || {};
    }
  }

  let languageMappings = {};

  function normalizeLanguageName(language) {
    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();

    async function replaceOtherIcon(targetSvg) {
      try {
        const response = await fetch(`${ICON_BASE_URL}other.svg`);
        const svgText = await response.text();
        
        const parser = new DOMParser();
        const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
        const newSvg = svgDoc.querySelector('svg');
        
        const attributes = targetSvg.attributes;
        for (let i = 0; i < attributes.length; i++) {
          const attr = attributes[i];
          if (attr.name !== 'class') {
            newSvg.setAttribute(attr.name, attr.value);
          }
        }
        
        newSvg.setAttribute('width', '16');
        newSvg.setAttribute('height', '16');
        newSvg.setAttribute('viewBox', '0 0 24 24');
        
        const innerGroup = newSvg.querySelector('g') || newSvg.querySelector('path');
        if (innerGroup) {
          innerGroup.setAttribute('transform', 'scale(0.67)');
        }
        
        const targetClasses = Array.from(targetSvg.classList);
        targetClasses.forEach(className => {
          if (className !== 'mr-2') {
            newSvg.classList.add(className);
          }
        });
        newSvg.classList.add('mr-1');
        
        newSvg.style.width = '16px';
        newSvg.style.height = '16px';
        newSvg.style.minWidth = '16px';
        newSvg.style.minHeight = '16px';
        
        targetSvg.parentNode.replaceChild(newSvg, targetSvg);
      } catch (error) {
        console.error('Failed to replace Other icon:', error);
      }
    }

    function processElement(element) {
      if (processedElements.has(element)) return;
      processedElements.add(element);

      let langElement, language;
      
      // Case 1: Action List Items
      if (element.matches('.ActionListItem-visual.ActionListItem-visual--leading')) {
        const languageLabel = element.closest('.ActionListContent')?.querySelector('.ActionListItem-label.text-normal');
        if (!languageLabel || !languageLabel.textContent || element.dataset.iconChecked) return;
        
        language = normalizeLanguageName(languageLabel.textContent);
        element.dataset.iconChecked = 'true';
        
        const colorDiv = element.querySelector('div');
        if (!colorDiv || !availableIcons.includes(language)) return;
        
        const img = document.createElement('img');
        img.src = `${ICON_BASE_URL}${language}.svg`;
        img.width = 16;
        img.height = 16;
        img.style.verticalAlign = 'middle';
        
        colorDiv.replaceWith(img);
      } 
      // Case 2: Inline Displays
      else 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);
      } 
      // Case 3: Repository Language Indicators
      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();
        }
      } 
      // Case 4: No Wrap Language Spans
      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);
        }
      } 
      // Case 5: File Type Boxes
      else if (element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
        const preElements = element.querySelectorAll('.Box-sc-g0xbh4-0');
        preElements.forEach(preElement => {
          preElement.style.display = 'none';
        });

        const languageSpan = element.querySelector('.Text__StyledText-sc-17v1xeu-0');
        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';
          }
        }
      } 
      // Case 6: Repository Language Colors
      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';
          }
        }
      } 
      // Case 7: Special File Type Boxes
      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';
          }
        }
      }
      // Case 8: Other Language Icons
      else if (element.matches('.octicon-dot-fill')) {
        const parentElement = element.closest('.d-inline');
        if (parentElement) {
          const languageElement = parentElement.querySelector('.text-bold');
          if (languageElement && languageElement.textContent.toLowerCase() === 'other' && !element.dataset.iconProcessed) {
            element.dataset.iconProcessed = 'true';
            replaceOtherIcon(element);
          }
        }
      }
    }

    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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill')) {
              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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').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, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').forEach(processElement);
  }

  // Case 9: File Browser Language Boxes
  function hideAndReplaceDivs() {
    document.querySelectorAll('span.Box-sc-g0xbh4-0.lnwIhU').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();
  }

  async function init() {
    languageMappings = await fetchLanguageMappings();
    replaceLanguageIcons();
    observeDOMChanges();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();