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 }
    );
})();