AtCoder Editorial for Typical90

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

Fra og med 13.07.2021. Se den nyeste version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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