Greasy Fork is available in English.

AtCoder Editorial for Typical90

AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。

// ==UserScript==
// @name         AtCoder Editorial for Typical90
// @namespace    https://github.com/KATO-Hiro
// @version      0.6.0
// @description  AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。
// @match        https://atcoder.jp/contests/typical90*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.5/dayjs.min.js
// @author       hiro_hiro
// @license      CC0
// @downloadURL
// @updateURL
// @homepage     https://github.com/KATO-Hiro/AtCoder-Editorial-for-Typical90
// @supportURL   https://github.com/KATO-Hiro/AtCoder-Editorial-for-Typical90/issues
// @grant        GM_addStyle
// ==/UserScript==

(async function () {
    "use strict";

    addTabs();

    const tasks = await fetchTasks(); // TODO: Use cache to reduce access to AtCoder.
    addEditorialPage(tasks);

    $(".nav-tabs a").click(function () {
        changeTab($(this));
        hideContentsOfPreviousPage();

        return false;
    });

    // TODO: 「解説」ボタンをクリックしたら、該当する問題のリンクを表示できるようにする
})();

function addTabs() {
    addTabContentStyles();
    addTabContents();
    addEditorialTab();
}

function addTabContentStyles() {
    const tabContentStyles = `
        .tab-content {
            display: none;
        }
        .tab-content.active {
            display: block;
        }
    `;

    GM_addStyle(tabContentStyles);
}

function addTabContents() {
    const contestNavTabsId = document.getElementById("contest-nav-tabs");

    // See:
    // https://stackoverflow.com/questions/268490/jquery-document-createelement-equivalent
    // https://blog.toshimaru.net/jqueryhidden-inputjquery/
    const idNames = [
        "editorial-created-by-userscript"
    ];

    for (const idName of idNames) {
        $("<div>", {
            class: "tab-content",
            id: idName,
        }).appendTo(contestNavTabsId);
    }
}

// FIXME: Hard coding is not good.
function addEditorialTab() {
    // See:
    // https://api.jquery.com/before/
    $("li.pull-right").before("<li><a href='#editorial-created-by-userscript'><span class='glyphicon glyphicon-book' style='margin-right:4px;' aria-hidden='true'></span>解説</a></li>");
}

function padZero(taskId) {
    // See:
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
    return String(taskId).padStart(3, '0');
}

// TODO: キャッシュを利用して、本家へのアクセスを少なくなるようにする
async function fetchTasks() {
    const tbodies = await fetchTaskPage();
    const tasks = new Object();
    let taskCount = 1;

    for (const [index, aTag] of Object.entries($(tbodies).find("a"))) {
        // Ignore a-tags including task-id and "Submit".
        // See:
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
        if (index % 3 == 1 && aTag.text.includes("★")) {
            const taskId = String(taskCount).padStart(3, "0");
            tasks[taskId] = [aTag.text, aTag.href];
            taskCount += 1;
        }
    }

    return tasks;
}

async function fetchTaskPage() {
    // See:
    // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
    // https://developer.mozilla.org/en-US/docs/Web/API/Body/text
    // https://developer.mozilla.org/ja/docs/Web/API/DOMParser
    // https://api.jquery.com/each/
    // http://dyn-web.com/tutorials/object-literal/properties.php#:~:text=Add%20a%20Property%20to%20an%20Existing%20Object%20Literal&text=myObject.,if%20it%20is%20a%20string).
    const tbodies = await fetch("https://atcoder.jp/contests/typical90/tasks", {
        method: "GET"
    })
    .then(response => {
        return response.text()
    })
    .then(html => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, "text/html");
        const messages = doc.querySelector("#main-container > div.row > div:nth-child(2) > div > table > tbody");

        return messages;
    })
    .catch(error => {
        console.warn('Something went wrong.', error);
    });

    return tbodies;
}

function addEditorialPage(tasks) {
    const editorialId = "#editorial-created-by-userscript";

    showHeader("editorial-header", "解説", editorialId);
    addHorizontalRule(editorialId);
    showDifficultyVotingAndUserCodes(editorialId);

    let taskEditorialsDiv = addDiv("task-editorials", editorialId);
    taskEditorialsDiv = "." + taskEditorialsDiv;
    addEditorials(tasks, taskEditorialsDiv);
}

function showHeader(className, text, tag) {
    addHeader(
        "<h2>", // heading_tag
        className, // className
        text, // text
        tag // parent_tag
    );
}

function addHeader(heading_tag, className, text, parent_tag) {
    $(heading_tag, {
        class: className,
        text: text,
    }).appendTo(parent_tag);
}

function addHorizontalRule(tag) {
    // See:
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr
    $("<hr>", {
        class: "",
    }).appendTo(tag);
}

function showDifficultyVotingAndUserCodes(tag) {
    addHeader(
        "<h3>", // heading_tag
        "difficulty-voting-and-user-codes", // className
        "問題の難易度を投票する・ソースコードを共有する", // text
        tag // parent_tag
    );

    $("<ul>", {
        class: "spread-sheets-ul",
        text: ""
    }).appendTo(tag);

    const spreadSheetUrl = "https://docs.google.com/spreadsheets/d/1GG4Higis4n4GJBViVltjcbuNfyr31PzUY_ZY1zh2GuI/edit#gid=";

    const homeID = "0";
    addSpreadSheetHomeURL(spreadSheetUrl + homeID);

    const difficultyVotingID = "1593175261";
    addDifficultyVotingURL(spreadSheetUrl + difficultyVotingID);

    const taskGroups = [
        ["001", "023", spreadSheetUrl + "105162261"], // task start, task end, spread sheet id.
        ["024", "047", spreadSheetUrl + "1671161250"],
        ["048", "071", spreadSheetUrl + "671876031"],
        ["072", "090", spreadSheetUrl + "428850451"]
    ];

    // See:
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
    taskGroups.forEach(
        taskGroup => {
            const taskStart = taskGroup[0];
            const taskEnd = taskGroup[1];
            const url = taskGroup[2];

            addUserCodesURL(
                taskStart,
                taskEnd,
                url
            );
        }
    );
}

function addSpreadSheetHomeURL(url) {
    $("<li>", {
        class: "spread-sheet-home-li",
        text: ""
    }).appendTo(".spread-sheets-ul");

    $("<a>", {
        class: "spread-sheet-home-url",
        href: url,
        text: "目的",
        target: "_blank",
        rel: "noopener",
    }).appendTo(".spread-sheet-home-li");
}

function addDifficultyVotingURL(url) {
    $("<li>", {
        class: "difficulty-voting-li",
        text: ""
    }).appendTo(".spread-sheets-ul");

    $("<a>", {
        class: "difficulty-voting-url",
        href: url,
        text: "問題の難易度を投票する",
        target: "_blank",
        rel: "noopener",
    }).appendTo(".difficulty-voting-li");
}

function addUserCodesURL(taskStart, taskEnd, url) {
    // See:
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals
    $("<li>", {
        class: `user-codes-${taskStart}-${taskEnd}-li`,
        text: ""
    }).appendTo(".spread-sheets-ul");

    $("<a>", {
        class: `user-codes-${taskStart}-${taskEnd}-url`,
        href: url,
        text: `ソースコード(${taskStart}〜${taskEnd})を見る・共有する`,
        target: "_blank",
        rel: "noopener",
    }).appendTo(`.user-codes-${taskStart}-${taskEnd}-li`);
}

function addDiv(tagName, parentTag) {
    $("<div>", {
        class: tagName,
    }).appendTo(parentTag);

    return tagName;
}

function addEditorials(tasks, parentTag) {
    const githubRepoUrl = getGitHubRepoUrl();
    const editorialsUrl = githubRepoUrl + "editorial/";
    const codesUrl = githubRepoUrl + "sol/";

    // See:
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
    const latestTaskId = Object.keys(tasks).slice(-1)[0];

    // HACK: 公開当日分の問題についてはリンク切れを回避するため、解説・ソースコードの一覧を示すことで応急的に対処
    // HACK: 問題によっては、複数の解説とソースコードが公開される日もある
    // getMultipleEditorialUrlsIfNeeds()とgetMultipleCodeUrls()で、アドホック的に対処している
    for (const [taskId, [taskName, taskUrl]] of Object.entries(tasks)) {
        let taskEditorialDiv = addDiv(`task-${taskId}-editorial`, parentTag);
        taskEditorialDiv = "." + taskEditorialDiv;

        // See:
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
        // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split
        showTaskName(taskId, `${taskId} - ${taskName}`, taskUrl, taskEditorialDiv);

        const additionalUrls = getMultipleEditorialUrlsIfNeeds(taskId);

        // TODO: AtCoderの解説ページで図を表示できるようにする
        for (const [index, additionalUrl] of Object.entries(additionalUrls)) {
            const editorialUrl = editorialsUrl + taskId + additionalUrl + ".jpg";
            showEditorial(taskId + additionalUrl, editorialUrl, additionalUrl, taskEditorialDiv);
        }

        const codeUrls = getMultipleCodeUrls(taskId);

        // TODO: ソースコードをフォーマットされた状態で表示する
        for (const [index, codeUrl] of Object.entries(codeUrls)) {
            const editorialCodelUrl = codesUrl + taskId + codeUrl;
            const [additionalUrl, language] = codeUrl.split(".");
            showCode(taskId + additionalUrl, editorialCodelUrl, codeUrl, taskEditorialDiv);
        }
    }
}

function getGitHubRepoUrl() {
    const url = "https://github.com/E869120/kyopro_educational_90/blob/main/";

    return url;
}

function showTaskName(taskId, taskName, taskUrl, tag) {
    const taskIdClass = `task-${taskId}`;

    addHeader(
        "<h3>", // heading_tag
        taskIdClass, // className
        taskName, // text
        tag // parent_tag
    );

    $("<a>", {
        class: `${`task-${taskId}-url`} small glyphicon glyphicon-new-window`,
        href: taskUrl,
        target: "_blank",
    }).appendTo(`.${taskIdClass}`);
}

// TODO: 複数の解説資料がアップロードされた日があれば更新する
function getMultipleEditorialUrlsIfNeeds(taskId) {
    // See:
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Property_Accessors
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in

    // タスク名: 解説ファイルの番号
    // 0xx-yyy.jpgの0xxをキーに、-yyyを値としている
    const multipleEditorialUrls = {
        "005": ["-01", "-02", "-03"],
        "011": ["-01", "-02"],
        "017": ["-01", "-02", "-03"],
        "023": ["-01", "-02", "-03", "-04"],
        "029": ["-01", "-02"],
        "035": ["-01", "-02", "-03"],
        "041": ["-01", "-02", "-03"],
        "047": ["-01", "-02"],
        "053": ["-01", "-02", "-03", "-04"],
        "059": ["-01", "-02", "-03"],
        "065": ["-01", "-02", "-03"],
        "071": ["-01", "-02", "-03"],
        "077": ["-01", "-02", "-03"],
        "083": ["-01", "-02", "-03", "-04"],
        "084": ["-01", "-02"],
        "085": ["-01", "-02"],
        "086": ["-01", "-02"],
        "087": ["-01", "-02"],
        "088": ["-01", "-02"],
        "089": ["-01", "-02", "-03", "-04"],
        "090": ["-01", "-02", "-03", "-04", "-05", "-06"],
    };

    if (taskId in multipleEditorialUrls) {
        return multipleEditorialUrls[taskId];
    } else {
        return [""]; // dummy
    }
}

// TODO: 複数の想定コードがアップロードされた日があれば更新する
function getMultipleCodeUrls(taskId) {
    // タスク名: ソースコードの番号と拡張子
    // 0xx-yyy.langの0xxをキーに、-yyy.langを値としている
    const multipleCodeUrls = {
        "005": ["-01.cpp", "-02.cpp", "-03.cpp"],
        "011": ["-01.cpp", "-02.cpp", "-03.cpp"],
        "017": ["-01.cpp", "-02.cpp", "-03.cpp"],
        "023": ["-01.cpp", "-02.cpp", "-03.cpp", "-04a.cpp", "-04b.cpp"],
        "029": ["-01.cpp", "-02.cpp", "-03.cpp"],
        "035": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"],
        "041": ["-01a.cpp", "-01b.cpp", "-02.cpp", "-03.cpp"],
        "047": ["-01.cpp", "-02.cpp"],
        "053": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"],
        "055": [".cpp", "-02.py", "-03.py"],
        "059": ["-01.cpp", "-02.cpp"],
        "061": ["-01.cpp", "-02.cpp"],
        "065": ["-01.cpp", "-02.cpp", "-03.cpp"],
        "066": ["a.cpp", "b.cpp"],
        "068": ["a.cpp", "b.cpp"],
        "071": ["-02.cpp", "-03.cpp"],
        "077": ["-01.cpp", "-02.cpp", "-03.cpp", "-04a.cpp", "-04b.cpp"],
        "080": ["a.cpp", "b.cpp"],
        "082": ["a.cpp", "b.cpp"],
        "083": ["-01.cpp", "-02a.cpp", "-02b.cpp"],
        "084": ["-01.cpp", "-02.cpp"],
        "089": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp", "-05.cpp"],
        "090": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp", "-05.cpp", "-06a.cpp", "-06b.cpp", "-07a.cpp", "-07b.cpp"],
    };

    if (taskId in multipleCodeUrls) {
        return multipleCodeUrls[taskId];
    } else {
        return [".cpp"];
    }
}

function addNote(className, message, parent_tag) {
    $("<p>", {
        class: className,
        text: message,
    }).appendTo(parent_tag);
}

function showEditorial(taskId, url, additionalUrl, tag) {
    const ulClass = `editorial-${taskId}-ul`;
    const liClass = `editorial-${taskId}-li`;

    $("<ul>", {
        class: ulClass,
        text: ""
    }).appendTo(tag);

    $("<li>", {
        class: liClass,
        text: ""
    }).appendTo(`.${ulClass}`);

    $("<a>", {
        class: `editorial-${taskId}-url`,
        href: url,
        text: `公式解説${additionalUrl}`,
        target: "_blank",
        rel: "noopener",
    }).appendTo(`.${liClass}`);
}

function showCode(taskId, url, additionalUrl, tag) {
    const ulClass = `editorial-${taskId}-code-ul`;
    const liClass = `editorial-${taskId}-code-li`;

    $("<ul>", {
        class: ulClass,
        text: ""
    }).appendTo(tag);

    $("<li>", {
        class: liClass,
        text: ""
    }).appendTo(`.${ulClass}`);

    $("<a>", {
        class: `editorial-${taskId}-code-url`,
        href: url,
        text: `想定ソースコード${additionalUrl}`,
        target: "_blank",
        rel: "noopener",
    }).appendTo(`.${liClass}`);
}

function addEditorialButtonToTaskPage() {
    // See:
    // https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
    // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
    const editorialButton = document.createElement("a");
    editorialButton.classList.add("btn", "btn-default", "btn-sm");
    editorialButton.textContent = "解説";

    const taskTitle = document.querySelector(".row > div > .h2");

    if (taskTitle) {
        taskTitle.appendChild(editorialButton);
        return editorialButton;
    } else {
        return;
    }
}

function changeTab(this_object) {
    // See:
    // https://api.jquery.com/parent/
    // https://api.jquery.com/addClass/#addClass-className
    // https://api.jquery.com/siblings/#siblings-selector
    // https://api.jquery.com/removeClass/#removeClass-className
    // https://www.design-memo.com/coding/jquery-tab-change
    this_object.parent().addClass("active").siblings(".active").removeClass("active");
    const tabContentsUrl = this_object.attr("href");
    $(tabContentsUrl).addClass("active").siblings(".active").removeClass("active");
}

function hideContentsOfPreviousPage() {
    // See:
    // https://api.jquery.com/length/
    // https://api.jquery.com/hide/
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for
    // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String
    const tagCount = $(".col-sm-12").length;

    for (let index = 0; index < tagCount; index++) {
        if (index != 0) {
            $("#main-container > div.row > div:nth-child(" + String(index + 1) + ")").hide();
        }
    }
}