Greasy Fork is available in English.

atcoder-standings-lang

AtCoder の順位表に最多提出言語を追加します.uesugi6111 さん作のスクリプトが元ネタです.

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         atcoder-standings-lang
// @namespace    iilj
// @version      2020.11.12.0
// @description  AtCoder の順位表に最多提出言語を追加します.uesugi6111 さん作のスクリプトが元ネタです.
// @author       iilj
// @supportURL   https://github.com/iilj/atcoder-standings-lang/issues
// @match        https://atcoder.jp/contests/*/standings*
// ==/UserScript==

/* globals $ */

/**
 * ユーザID/言語ごとの提出数
 * @typedef {Object} UserLangEntry
 * @property {string} user_id ユーザ ID
 * @property {string} language 言語名
 * @property {number} count 提出数
 */

(() => {
    'use strict';

    /** @type {Map<string, UserLangEntry[]>} */
    const userLangEntryMap = new Map();

    const fetchLangJson = async () => {
        console.log('@kenkooooさんありがとう');
        console.log('うえすぎさんありがとう');
        // 2e5 個くらい要素があるのでキャッシュする
        const res = await fetch("https://kenkoooo.com/atcoder/resources/lang.json", { cache: 'force-cache' });
        /** @type {UserLangEntry[]} */
        const userLangEntries = await res.json();

        // prepare map
        userLangEntries.forEach(userLangEntry => {
            if (userLangEntryMap.has(userLangEntry.user_id)) {
                userLangEntryMap.get(userLangEntry.user_id).push(userLangEntry);
            } else {
                userLangEntryMap.set(userLangEntry.user_id, [userLangEntry]);
            }
        });

        // sort arrays
        userLangEntryMap.forEach(userLangArray => {
            userLangArray.sort((a, b) => b.count - a.count); // in place
        });
    };

    /** @type {(anchor: HTMLAnchorElement) => void} */
    const updateAnchor = (anchor) => {
        if (!anchor.href.includes('/users/')) return;

        const user_id = anchor.text.trim();
        if (!userLangEntryMap.has(user_id)) return;

        const userLangArray = userLangEntryMap.get(user_id);

        const tooltipHtml = userLangArray.map((userLangEntry) =>
            `${userLangEntry.language} : ${userLangEntry.count}`
        ).join('<br>');

        let langHtml = userLangArray[0].language;
        if (userLangArray.length >= 2) {
            langHtml += '<div style="font-size:10px;display:inline;">'
                + `/${userLangArray[1].language}`
                + (userLangArray.length >= 3 ? `/${userLangArray[2].language}` : '')
                + ' </div>';
        }

        anchor.insertAdjacentHTML('beforeend',
            '/'
            + '<div data-toggle="tooltip" data-html="true" data-placement="right" style="font-size:12px;display:inline;" title="'
            + tooltipHtml + '">'
            + langHtml
            + '</div>');
    };

    /** @type {(tbody: HTMLTableSectionElement) => void} */
    const updateTable = (tbody) => {
        tbody.querySelectorAll('.username').forEach(anchor => {
            updateAnchor(anchor);
        });
        $('[data-toggle="tooltip"]').tooltip();
    };

    /** @type {HTMLTableSectionElement} */
    let tbody = null;

    const tableObserver = new MutationObserver(() => {
        updateTable(tbody);
    });
    const parentObserver = new MutationObserver(async () => {
        tbody = document.getElementById('standings-tbody');
        if (tbody) {
            parentObserver.disconnect();
            await fetchLangJson();
            updateTable(tbody);
            tableObserver.observe(
                tbody,
                { childList: true }
            )
        }
    });
    parentObserver.observe(
        document.getElementById('vue-standings'),
        { childList: true, subtree: true }
    );
})();