Linguist Expand

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
        });
    }
})();