Linguist Expand

Expands Github's Linguist language list on repositories to show every language instead of hiding the small percentage under "Other"

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Linguist Expand
// @namespace https://davoleo.net
// @author Davoleo
// @homepage https://github.com/Davoleo/scripts/tree/master/linguist_expand
// @description Expands Github's Linguist language list on repositories to show every language instead of hiding the small percentage under "Other"
// @contributionURL https://davoleo.net/donate
// @match https://github.com/*
// @require https://unpkg.com/[email protected]/dist/js-yaml.min.js
// @resource languageColors https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml
// @grant GM_getResourceText
// @grant GM_log
// @grant GM_xmlhttpRequest
// @connect api.github.com
// @run-at document-idle
// @version 1.0.2
// @license MIT
// @noframes
// ==/UserScript==

//Loads languages.yml (from Github's linguist repo) the most updated, official and complete collection of github languages and their colors
//loaded into a JS object via jsyaml (a library to parse yaml inside of javascript)
const languages = jsyaml.load(GM_getResourceText('languageColors'));

//Contains information about languages and their percentages in the repository
let langPercentagesMap = {}
//Contains information about languages and their colors
let langColorsMap = {}

/**
 * Function to standardize and modernize GM_xmlhttpRequest to work with promises
 * @param {String} url of the endpoint
 * @param {Object} options Contains extra information about the request
 * @returns a promise with the requested content
 */
function request(url, options={}) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      url,
      method: options.method || "GET",
      headers: options.headers || {Accept: "application/json",
                                   "Content-Type": "application/json"},
      responseType: options.responseType || "json",
      data: options.body || options.data,
      onload: res => resolve(res.response),
      onerror: reject
    })
  })
}

/**
 * Retrieve information about the languages of a repository via the Github API
 * @param {String} user owner of the repository
 * @param {String} repo name
 * @returns the languages of the repository as a JS Object | null if the promise is rejected for any reason.
 */
async function retrieveLanguages(user, repo) {
    try {
        return await request(`https://api.github.com/repos/${user}/${repo}/languages`, {
            headers: {
                Accept: "application/vnd.github.v3+json"
            }
        });
    }
    catch(e) {
        return null;
    }
}

/**
 * Builds language bar segments assigning the correct colors and width depending on the language and it's frequency in the repository
 * @param {string} name of the language
 * @param {string} color of the language
 * @param {number} percentage of the language in the repository code
 * @returns a segment span of the language bar with the correct width and color
 */
function buildBarSegmentSpan(name, color, percentage) {
    const segment = document.createElement('span');
    segment.style.setProperty('background-color', color, 'important');
    segment.style.width = percentage + '%';
    //Removes any margin which would make the language bar otherwise inaccurate
    segment.style.setProperty('margin', '0', 'important');
    //Make sure there's at least 1px of width in the bar segment (fixes width of 0.0% segments)
    //TODO: investigate a better way to do this
    segment.style.paddingLeft = '1px';
    segment.setAttribute("itemprop", "keywords");
    segment.setAttribute("aria-label", name + ' ' + percentage);
    segment.setAttribute("data-view-component", "true");
    segment.setAttribute("class", "Progress-item color-bg-success-inverse lingustexpand");
    return segment;
}

/**
 * Builds a chip for each language containing
 * - The Color of the language in the bar
 * - The Name of the language
 * - The Percentage of the language in repository files
 * @param {String} owner of the repository
 * @param {String} repo name
 * @param {String} name of the language
 * @param {String} color of the language
 * @param {number} percentage percentage of the language in the repository code
 * @returns A chip components featured as legend for the language bar
 */
function buildLanguageChip(owner, repo, name, color, percentage) {
    const chip = document.createElement('li');
    chip.classList.add('d-inline');

    const chipLink = document.createElement('a');
    chipLink.classList.add('d-inline-flex', 'flex-items-center', 'flex-nowrap', 'Link--secondary', 'no-underline', 'text-small', 'mr-3');
    chipLink.href = `/${owner}/${repo}/search?l=${name}` //Chip link should bring you to the search query with the correct language in place

    //Parse SVG BALL directly injecting the correct color as in-line style
    const svgText = `
    <svg style="color:${color};" aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" 
            width="16" data-view-component="true" class="octicon octicon-dot-fill mr-2">
        <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path>
    </svg>
    `;
    const svgTMP = document.createElement('template');
    svgTMP.innerHTML = svgText;
    chipLink.append(svgTMP.content);
    //^ uses a template HTMLElement to parse HTML into its respective DOM elements

    //Adds language name to the chip
    const chipName = document.createElement('span');
    chipName.classList.add('color-fg-default', 'text-bold', 'mr-1');
    chipName.textContent = name;
    chipLink.append(chipName);

    //Adds Language percentage to the chip
    const chipValue = document.createElement('span');
    chipValue.textContent = percentage + '%';
    chipLink.append(chipValue);

    chip.append(chipLink);
    return chip;
}

/**
 * Builds the custom language stats section and returns it
 * @returns The full section with complete repository language stats
 */
function buildLanguagesSection(owner, repo) {

    const languageSection = document.createElement("div");
    languageSection.classList.add("mb-3", "mt-1");

    const bar = document.createElement('span');
    bar.classList.add("Progress", 'mb-2');
    bar.setAttribute("data-view-component", "true");
    Object.keys(langColorsMap).forEach((lang, i) => {
        const segment = buildBarSegmentSpan(lang, langColorsMap[lang], langPercentagesMap[lang]);
        //if (i !== 0) {
        //    segment.style.setProperty('margin-left', '1px');
        //}
        bar.appendChild(segment);
    });
    languageSection.append(bar);

    const languageUL = document.createElement('ul');
    Object.keys(langColorsMap).forEach((lang) => {
        const languageChip = buildLanguageChip(owner, repo, lang, langColorsMap[lang], langPercentagesMap[lang]);
        languageUL.append(languageChip);
    });
    languageSection.append(languageUL);

    return languageSection;
}

//MAIN ENTRY POINT
(() => {
    'use strict';

    //Selects the box element that contains files and folders on the repo page
    const mainContent = document.querySelector(".Box-sc-g0xbh4-0.yfPnm");
    if (!mainContent)
        throw Error("mainContent Hook Selector is dead!")

    //The original language bar in the sidebar
    const originalLangBar = document.querySelector("div.Layout-sidebar span.Progress");

    //array that is generated from the tab URL, it's structured this way: ["", "<repo_owner>", "<repo_name>"]
    const ownerRepo = window.location.pathname.split('/');

    //only works against github.com/ABC/DEF links
    if (ownerRepo.length === 3) {
        //retrieves necessary information about the repository's languages
        retrieveLanguages(ownerRepo[1], ownerRepo[2]).then((lang_vals) => {
            //assume request is successful if object is not null and it doesn't contain 'message' in its keys
            if (lang_vals !== null && !lang_vals.message) {
                //Sum of all language values
                const total = Object.values(lang_vals).reduce((prev, curr) => prev + curr);
                //for each language in the object
                Object.keys(lang_vals).forEach((lang) => {
                    //
                    langColorsMap[lang] = languages[lang].color;
                    langPercentagesMap[lang] = ((lang_vals[lang] / total) * 100).toFixed(1);
                });
            }
            else return; //Short Circuit

            //Build the new custom lang stats
            const languageSection = buildLanguagesSection(ownerRepo[1], ownerRepo[2]);
            mainContent.insertAdjacentElement('beforebegin', languageSection);
            //^ inserts our custom language stats before the box containing directories and files

            //GM_log(langColorsMap);
            //GM_log(langPercentagesMap);

            //Remove original Language Section (sidebar)
            originalLangBar.parentElement.parentElement.remove();
        });
    }
})();