comfortable-yukicoder

yukicoder にいくつかの機能を追加します.主に動線を増やします.

// ==UserScript==
// @name         comfortable-yukicoder
// @namespace    iilj
// @version      1.1.1
// @description  yukicoder にいくつかの機能を追加します.主に動線を増やします.
// @author       iilj
// @license      MIT
// @supportURL   https://github.com/iilj/comfortable-yukicoder/issues
// @match        https://yukicoder.me/contests/*
// @match        https://yukicoder.me/contests/*/*
// @match        https://yukicoder.me/problems/no/*
// @match        https://yukicoder.me/problems/*
// @match        https://yukicoder.me/submissions/*
// @grant        GM_addStyle
// ==/UserScript==
var css$1 = "div#js-cy-timer {\n  position: fixed;\n  right: 10px;\n  bottom: 10px;\n  width: 140px;\n  height: 70px;\n  margin: 0;\n  text-align: center;\n  line-height: 20px;\n  font-size: 15px;\n  z-index: 50;\n  border: 7px solid #36353a;\n  border-radius: 7px;\n  background-color: #bdc4bd;\n  padding: 8px 0;\n}";
 
const pad = (num, length = 2) => `00${num}`.slice(-length);
const days = ['日', '月', '火', '水', '木', '金', '土'];
const formatDate = (date, format = '%Y-%m-%d (%a) %H:%M:%S.%f %z') => {
    const offset = date.getTimezoneOffset();
    const offsetSign = offset < 0 ? '+' : '-';
    const offsetHours = Math.floor(Math.abs(offset) / 60);
    const offsetMinutes = Math.abs(offset) % 60;
    let ret = format.replace(/%Y/g, String(date.getFullYear()));
    ret = ret.replace(/%m/g, pad(date.getMonth() + 1));
    ret = ret.replace(/%d/g, pad(date.getDate()));
    ret = ret.replace(/%a/g, days[date.getDay()]);
    ret = ret.replace(/%H/g, pad(date.getHours()));
    ret = ret.replace(/%M/g, pad(date.getMinutes()));
    ret = ret.replace(/%S/g, pad(date.getSeconds()));
    ret = ret.replace(/%f/g, pad(date.getMilliseconds(), 3));
    ret = ret.replace(/%z/g, `${offsetSign}${pad(offsetHours)}:${pad(offsetMinutes)}`);
    return ret;
};
const formatTime = (hours, minutes, seconds) => {
    return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
};
 
const diffMsToString = (diffMs) => {
    const diffWholeSecs = Math.ceil(diffMs / 1000);
    const diffSecs = diffWholeSecs % 60;
    const diffMinutes = Math.floor(diffWholeSecs / 60) % 60;
    const diffHours = Math.floor(diffWholeSecs / 3600) % 24;
    const diffDate = Math.floor(diffWholeSecs / (3600 * 24));
    const diffDateText = diffDate > 0 ? `${diffDate}日と` : '';
    return diffDateText + formatTime(diffHours, diffMinutes, diffSecs);
};
class Timer {
    constructor() {
        GM_addStyle(css$1);
        this.element = document.createElement('div');
        this.element.id = Timer.ELEMENT_ID;
        document.body.appendChild(this.element);
        this.top = document.createElement('div');
        this.element.appendChild(this.top);
        this.bottom = document.createElement('div');
        this.element.appendChild(this.bottom);
        this.prevSeconds = -1;
        this.startDate = undefined;
        this.endDate = undefined;
        this.intervalID = window.setInterval(() => {
            this.updateTime();
        }, 100);
    }
    updateTime() {
        const d = new Date();
        const seconds = d.getSeconds();
        if (seconds === this.prevSeconds)
            return;
        this.prevSeconds = seconds;
        if (this.startDate !== undefined && this.endDate !== undefined) {
            if (d < this.startDate) {
                this.top.textContent = '開始まであと';
                const diffMs = this.startDate.getTime() - d.getTime();
                this.bottom.textContent = diffMsToString(diffMs);
            }
            else if (d < this.endDate) {
                this.top.textContent = '残り時間';
                const diffMs = this.endDate.getTime() - d.getTime();
                this.bottom.textContent = diffMsToString(diffMs);
            }
            else {
                this.top.textContent = formatDate(d, '%Y-%m-%d (%a)');
                this.bottom.textContent = formatDate(d, '%H:%M:%S %z');
            }
        }
        else {
            this.top.textContent = formatDate(d, '%Y-%m-%d (%a)');
            this.bottom.textContent = formatDate(d, '%H:%M:%S %z');
        }
    }
    registerContest(contest) {
        this.startDate = new Date(contest.Date);
        this.endDate = new Date(contest.EndDate);
    }
}
Timer.ELEMENT_ID = 'js-cy-timer';
 
var css = "#toplinks > div#cy-tabs-container > a {\n  position: relative;\n  background: linear-gradient(to bottom, white 0%, #fff2f3 100%);\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul {\n  margin: 0;\n  padding: 0;\n  list-style-type: none;\n  overflow: hidden;\n  position: absolute;\n  left: 0;\n  top: 33px;\n  width: max-content;\n  min-height: 0;\n  height: 0;\n  z-index: 3;\n  transition: min-height 0.4s;\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul > li > a {\n  width: 100%;\n  height: 100%;\n  display: block;\n  margin: 0;\n  padding: 0.3rem;\n  padding-left: 0.6rem;\n  padding-right: 0.6rem;\n  font-size: 16px;\n  color: #fff;\n  line-height: 1.75;\n  background-color: #428bca;\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul > li > a:hover {\n  background-color: #3071a9;\n}\n#toplinks > div#cy-tabs-container > a:hover {\n  opacity: 1;\n}\n#toplinks > div#cy-tabs-container > a:hover ul.js-cy-contest-problems-ul {\n  height: auto;\n}";
 
const header = [
    'A',
    'B',
    'C',
    'D',
    'E',
    'F',
    'G',
    'H',
    'I',
    'J',
    'K',
    'L',
    'M',
    'N',
    'O',
    'P',
    'Q',
    'R',
    'S',
    'T',
    'U',
    'V',
    'W',
    'X',
    'Y',
    'Z',
];
const getHeaderFromNum = (num) => {
    const idx = num - 1;
    if (idx < header.length) {
        return header[idx];
    }
    else {
        const r = idx % header.length;
        return getHeaderFromNum(Math.floor(idx / header.length)) + header[r];
    }
};
const getHeader = (idx) => getHeaderFromNum(idx + 1);
 
class TopLinksManager {
    constructor() {
        GM_addStyle(css);
        const toplinks = document.querySelector('div#toplinks');
        if (toplinks === null) {
            throw Error('div#toplinks が見つかりません');
        }
        this.tabContainer = document.createElement('div');
        this.tabContainer.classList.add('left');
        this.tabContainer.id = TopLinksManager.TAB_CONTAINER_ID;
        toplinks.insertAdjacentElement('beforeend', this.tabContainer);
        this.id2element = new Map();
    }
    initLink(txt, id, href = '#') {
        const newtab = document.createElement('a');
        newtab.innerText = txt;
        newtab.id = id;
        newtab.setAttribute('href', href);
        this.tabContainer.appendChild(newtab);
        this.id2element.set(id, newtab);
        return newtab;
    }
    confirmLink(id, href) {
        const tab = this.id2element.get(id);
        if (tab === undefined) {
            throw new Error(`不明な id: ${id}`);
        }
        tab.href = href;
    }
    initContestSubmissions() {
        this.initLink('自分の提出', TopLinksManager.ID_CONTEST_SUBMISSION);
    }
    confirmContestSubmissions(contestId) {
        this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/contests/${contestId}/submissions?my_submission=enabled`);
    }
    initContestProblems() {
        this.initLink('コンテスト問題一覧', TopLinksManager.ID_CONTEST);
    }
    confirmContestProblems(contestId, contestProblems) {
        this.confirmLink(TopLinksManager.ID_CONTEST, `/contests/${contestId}`);
        this.addContestProblems(contestProblems);
    }
    initContestLinks() {
        this.initContestProblems();
        this.initLink('コンテスト順位表', TopLinksManager.ID_CONTEST_TABLE);
        this.initContestSubmissions();
    }
    confirmContestLinks(contestId, contestProblems) {
        this.confirmLink(TopLinksManager.ID_CONTEST_TABLE, `/contests/${contestId}/table`);
        this.confirmContestSubmissions(contestId);
        this.confirmContestProblems(contestId, contestProblems);
    }
    addContestProblems(contestProblems) {
        const tab = this.id2element.get(TopLinksManager.ID_CONTEST);
        if (tab === undefined) {
            throw new Error(`id=${TopLinksManager.ID_CONTEST} の要素が追加される前に更新が要求されました`);
        }
        const ul = document.createElement('ul');
        ul.classList.add('js-cy-contest-problems-ul');
        console.log(contestProblems);
        contestProblems.forEach((problem, index) => {
            console.log(problem);
            const li = document.createElement('li');
            const link = document.createElement('a');
            const header = getHeader(index);
            link.textContent = `${header} - ${problem.Title}`;
            if (problem.No !== null) {
                link.href = `/problems/no/${problem.No}`;
            }
            else {
                link.href = `/problems/${problem.ProblemId}`;
            }
            li.appendChild(link);
            ul.appendChild(li);
        });
        // add caret
        const caret = document.createElement('span');
        caret.classList.add('caret');
        tab.appendChild(caret);
        tab.insertAdjacentElement('beforeend', ul);
    }
    confirmWithoutContest(problem) {
        [TopLinksManager.ID_CONTEST, TopLinksManager.ID_CONTEST_TABLE].forEach((id) => {
            const tab = this.id2element.get(id);
            if (tab !== undefined)
                tab.remove();
        });
        // https://yukicoder.me/problems/no/5000/submissions?my_submission=enabled
        if (problem.No !== null) {
            this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/problems/no/${problem.No}/submissions?my_submission=enabled`);
        }
        else {
            this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/problems/${problem.ProblemId}/submissions?my_submission=enabled`);
        }
    }
}
TopLinksManager.TAB_CONTAINER_ID = 'cy-tabs-container';
TopLinksManager.ID_CONTEST = 'js-cy-contest';
TopLinksManager.ID_CONTEST_TABLE = 'js-cy-contest-table';
TopLinksManager.ID_CONTEST_SUBMISSION = 'js-cy-contest-submissions';
 
const onContestPage = async (contestId, APIClient) => {
    const toplinksManager = new TopLinksManager();
    toplinksManager.initContestSubmissions();
    toplinksManager.confirmContestSubmissions(contestId);
    const timer = new Timer();
    const contest = await APIClient.fetchContestById(contestId);
    timer.registerContest(contest);
};
 
const getContestProblems = (contest, problems) => {
    const pid2problem = new Map();
    problems.forEach((problem) => {
        pid2problem.set(problem.ProblemId, problem);
    });
    const contestProblems = contest.ProblemIdList.map((problemId) => {
        const problem = pid2problem.get(problemId);
        if (problem !== undefined)
            return problem;
        return {
            No: null,
            ProblemId: problemId,
            Title: '',
            AuthorId: -1,
            TesterId: -1,
            TesterIds: '',
            Level: 0,
            ProblemType: 0,
            Tags: '',
            Date: null,
            Statistics: {
                //
            },
        };
    });
    return contestProblems;
};
const anchorToUserID = (anchor) => {
    const userLnkMatchArray = /^https:\/\/yukicoder\.me\/users\/(\d+)/.exec(anchor.href);
    if (userLnkMatchArray === null)
        return -1;
    const userId = Number(userLnkMatchArray[1]);
    return userId;
};
const getYourUserId = () => {
    const yourIdLnk = document.querySelector('#header #usermenu-btn');
    if (yourIdLnk === null)
        return -1; // ログインしていない場合
    return anchorToUserID(yourIdLnk);
};
 
const onLeaderboardPage = async (contestId, APIClient) => {
    const myRankTableRow = document.querySelector('table.table tbody tr.my_rank');
    if (myRankTableRow !== null) {
        const myRankTableRowCloned = myRankTableRow.cloneNode(true);
        const tbody = document.querySelector('table.table tbody');
        if (tbody === null) {
            throw new Error('順位表テーブルが見つかりません');
        }
        tbody.insertAdjacentElement('afterbegin', myRankTableRowCloned);
        // const myRankTableFirstRow: HTMLTableRowElement | null =
        //     document.querySelector<HTMLTableRowElement>('table.table tbody tr.my_rank');
        // myRankTableFirstRow.style.borderBottom = '2px solid #ddd';
        myRankTableRowCloned.style.borderBottom = '2px solid #ddd';
    }
    const toplinksManager = new TopLinksManager();
    toplinksManager.initContestProblems();
    toplinksManager.initContestSubmissions();
    toplinksManager.confirmContestSubmissions(contestId);
    const timer = new Timer();
    const [problems, contest] = await Promise.all([APIClient.fetchProblems(), APIClient.fetchContestById(contestId)]);
    timer.registerContest(contest);
    const contestProblems = getContestProblems(contest, problems);
    toplinksManager.confirmContestProblems(contest.Id, contestProblems);
};
 
const createCard = () => {
    const newdiv = document.createElement('div');
    // styling newdiv
    newdiv.style.display = 'inline-block';
    newdiv.style.borderRadius = '2px';
    newdiv.style.padding = '10px';
    newdiv.style.margin = '10px 0px';
    newdiv.style.border = '1px solid rgb(59, 173, 214)';
    newdiv.style.backgroundColor = 'rgba(120, 197, 231, 0.1)';
    const newdivWrapper = document.createElement('div');
    newdivWrapper.appendChild(newdiv);
    return [newdiv, newdivWrapper];
};
 
class ContestInfoCard {
    constructor(isProblemPage = true) {
        this.isProblemPage = isProblemPage;
        const [card, cardWrapper] = createCard();
        this.card = card;
        {
            // create newdiv
            this.contestDiv = document.createElement('div');
            // add contest info
            this.contestLnk = document.createElement('a');
            this.contestLnk.innerText = '(fetching contest info...)';
            this.contestLnk.href = '#';
            this.contestDiv.appendChild(this.contestLnk);
            this.contestSuffix = document.createTextNode(` (id=---)`);
            this.contestDiv.appendChild(this.contestSuffix);
            // add problem info
            if (isProblemPage) {
                const space = document.createTextNode(` `);
                this.contestDiv.appendChild(space);
                this.problemLnk = document.createElement('a');
                this.problemLnk.innerText = '#?';
                this.problemLnk.href = '#';
                this.contestDiv.appendChild(this.problemLnk);
                this.problemSuffix = document.createTextNode(' (No.---)');
                this.contestDiv.appendChild(this.problemSuffix);
            }
            this.dateDiv = document.createElement('div');
            this.dateDiv.textContent = 'xxxx-xx-xx xx:xx:xx 〜 xxxx-xx-xx xx:xx:xx';
            // newdiv.innerText = `${contest.Name} (id=${contest.Id}) #${label} (No.${problem.No})`;
            card.appendChild(this.contestDiv);
            card.appendChild(this.dateDiv);
            if (isProblemPage) {
                this.prevNextProblemLinks = document.createElement('div');
                this.prevNextProblemLinks.textContent = '(情報取得中)';
                card.appendChild(this.prevNextProblemLinks);
            }
        }
        const content = document.querySelector('div#content');
        if (content === null) {
            throw new Error('div#content が見つかりませんでした');
        }
        content.insertAdjacentElement('afterbegin', cardWrapper);
    }
    confirmContest(contest) {
        this.contestLnk.innerText = `${contest.Name}`;
        this.contestLnk.href = `/contests/${contest.Id}`;
        this.contestSuffix.textContent = ` (id=${contest.Id})`;
        const format = '%Y-%m-%d (%a) %H:%M:%S';
        const start = formatDate(new Date(contest.Date), format);
        const end = formatDate(new Date(contest.EndDate), format);
        this.dateDiv.textContent = `${start} 〜 ${end}`;
    }
    confirmContestAndProblem(contest, problem, suffix = '') {
        this.confirmContest(contest);
        if (this.isProblemPage) {
            if (this.prevNextProblemLinks === undefined) {
                throw new ErrorEvent('prevNextProblemLinks が undefined です');
            }
            if (this.problemLnk === undefined) {
                throw new ErrorEvent('problemLnk が undefined です');
            }
            if (this.problemSuffix === undefined) {
                throw new ErrorEvent('problemSuffix が undefined です');
            }
            const idx = contest.ProblemIdList.findIndex((problemId) => problemId === problem.ProblemId);
            const label = getHeader(idx);
            this.problemLnk.innerText = `#${label}`;
            if (problem.No !== null) {
                this.problemLnk.href = `/problems/no/${problem.No}`;
                this.problemSuffix.textContent = ` (No.${problem.No})`;
            }
            else {
                this.problemLnk.href = `/problems/${problem.ProblemId}`;
            }
            this.prevNextProblemLinks.textContent = ' / ';
            if (idx > 0) {
                // prev
                const lnk = document.createElement('a');
                lnk.innerText = `←前の問題 (#${getHeader(idx - 1)})`;
                lnk.href = `/problems/${contest.ProblemIdList[idx - 1]}${suffix}`;
                this.prevNextProblemLinks.insertAdjacentElement('afterbegin', lnk);
            }
            if (idx + 1 < contest.ProblemIdList.length) {
                // next
                const lnk = document.createElement('a');
                lnk.innerText = `次の問題 (#${getHeader(idx + 1)})→`;
                lnk.href = `/problems/${contest.ProblemIdList[idx + 1]}${suffix}`;
                this.prevNextProblemLinks.insertAdjacentElement('beforeend', lnk);
            }
        }
    }
    confirmContestIsNotFound() {
        var _a, _b;
        this.contestLnk.remove();
        this.contestSuffix.remove();
        (_a = this.problemLnk) === null || _a === void 0 ? void 0 : _a.remove();
        (_b = this.problemSuffix) === null || _b === void 0 ? void 0 : _b.remove();
        this.dateDiv.remove();
        if (this.prevNextProblemLinks !== undefined) {
            this.prevNextProblemLinks.textContent = '(どのコンテストにも属さない問題です)';
        }
    }
    onProblemFetchFailed() {
        this.contestLnk.innerText = '???';
        if (this.prevNextProblemLinks !== undefined) {
            this.prevNextProblemLinks.textContent = '(情報が取得できませんでした)';
        }
    }
}
 
const onProblemPage = async (fetchProblem, suffix, APIClient) => {
    const toplinksManager = new TopLinksManager();
    toplinksManager.initContestLinks();
    const contestInfoCard = new ContestInfoCard();
    const timer = new Timer();
    try {
        const [problem, problems, currentContest, pastContest, futureContests] = await Promise.all([
            fetchProblem(),
            APIClient.fetchProblems(),
            APIClient.fetchCurrentContests(),
            APIClient.fetchPastContests(),
            APIClient.fetchFutureContests(),
        ]);
        const contests = currentContest.concat(pastContest);
        let contest = contests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
        if (contest === undefined) {
            // 未来のコンテストから探してみる
            if (problem.ProblemId !== undefined) {
                const futureContest = futureContests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
                if (futureContest !== undefined) {
                    contest = futureContest;
                    // print contest info
                    // contestInfoCard.confirmContestAndProblem(futureContest, problem, suffix);
                    // return null;
                }
                else {
                    contestInfoCard.confirmContestIsNotFound();
                    toplinksManager.confirmWithoutContest(problem);
                    return null;
                }
            }
            else {
                contestInfoCard.confirmContestIsNotFound();
                toplinksManager.confirmWithoutContest(problem);
                return null;
            }
        }
        const contestProblems = getContestProblems(contest, problems);
        // print contest info
        contestInfoCard.confirmContestAndProblem(contest, problem, suffix);
        // add tabs
        toplinksManager.confirmContestLinks(contest.Id, contestProblems);
        timer.registerContest(contest);
        return problem;
    }
    catch (error) {
        contestInfoCard.onProblemFetchFailed();
        return null;
    }
};
const onProblemPageByNo = async (problemNo, suffix, APIClient) => {
    return onProblemPage(() => APIClient.fetchProblemByNo(problemNo), suffix, APIClient);
};
const onProblemPageById = async (problemId, suffix, APIClient) => {
    return onProblemPage(() => APIClient.fetchProblemById(problemId), suffix, APIClient);
};
const colorScoreRow = (row, authorId, testerIds, yourId) => {
    const userLnk = row.querySelector('td.table_username a');
    if (userLnk === null) {
        throw new Error('テーブル行内にユーザへのリンクが見つかりませんでした');
    }
    const userId = anchorToUserID(userLnk);
    if (userId === -1)
        return;
    if (userId === authorId) {
        row.style.backgroundColor = 'honeydew';
        const label = document.createElement('div');
        label.textContent = '[作問者]';
        userLnk.insertAdjacentElement('afterend', label);
    }
    else if (testerIds.includes(userId)) {
        row.style.backgroundColor = 'honeydew';
        const label = document.createElement('div');
        label.textContent = '[テスター]';
        userLnk.insertAdjacentElement('afterend', label);
    }
    if (userId === yourId) {
        row.style.backgroundColor = 'aliceblue';
        const label = document.createElement('div');
        label.textContent = '[あなた]';
        userLnk.insertAdjacentElement('afterend', label);
    }
};
const onProblemScorePage = (problem) => {
    const yourId = getYourUserId();
    const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
    const rows = document.querySelectorAll('table.table tbody tr');
    rows.forEach((row) => {
        colorScoreRow(row, problem.AuthorId, testerIds, yourId);
    });
};
 
const colorSubmissionRow = (row, authorId, testerIds, yourId) => {
    const userLnk = row.querySelector('td.table_username a');
    if (userLnk === null) {
        throw new Error('テーブル行内にユーザへのリンクが見つかりませんでした');
    }
    const userId = anchorToUserID(userLnk);
    if (userId === -1)
        return;
    if (userId === authorId) {
        row.style.backgroundColor = 'honeydew';
        const label = document.createElement('div');
        label.textContent = '[作問者]';
        userLnk.insertAdjacentElement('afterend', label);
    }
    else if (testerIds.includes(userId)) {
        row.style.backgroundColor = 'honeydew';
        const label = document.createElement('div');
        label.textContent = '[テスター]';
        userLnk.insertAdjacentElement('afterend', label);
    }
    if (userId === yourId) {
        row.style.backgroundColor = 'aliceblue';
        const label = document.createElement('div');
        label.textContent = '[あなた]';
        userLnk.insertAdjacentElement('afterend', label);
    }
};
const onProblemSubmissionsPage = (problem) => {
    const yourId = getYourUserId();
    const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
    const rows = document.querySelectorAll('table.table tbody tr');
    rows.forEach((row) => {
        colorSubmissionRow(row, problem.AuthorId, testerIds, yourId);
    });
};
const onContestSubmissionsPage = async (contestId, APIClient) => {
    const toplinksManager = new TopLinksManager();
    toplinksManager.initContestProblems();
    toplinksManager.initContestSubmissions();
    const contestInfoCard = new ContestInfoCard(false);
    const yourId = getYourUserId();
    const [contest, problems] = await Promise.all([APIClient.fetchContestById(contestId), APIClient.fetchProblems()]);
    // print contest info
    contestInfoCard.confirmContest(contest);
    // add tabs
    const contestProblems = getContestProblems(contest, problems);
    toplinksManager.confirmContestProblems(contest.Id, contestProblems);
    toplinksManager.confirmContestSubmissions(contest.Id);
    const problemId2Label = contest.ProblemIdList.reduce((curMap, problemId, idx) => curMap.set(problemId, getHeader(idx)), new Map());
    const problemNo2ProblemMap = problems.reduce((curMap, problem) => {
        if (problem.No !== null)
            curMap.set(problem.No, problem);
        return curMap;
    }, new Map());
    // collect problemNos
    const rows = document.querySelectorAll('table.table tbody tr');
    for (let i = 0; i < rows.length; i++) {
        const row = rows[i];
        // add label to each problem link
        const lnk = row.querySelector('td a[href^="/problems/no/"]');
        if (lnk === null) {
            throw new Error('テーブル行内に問題へのリンクが見つかりませんでした');
        }
        const contestSubmissionsPageProblemLnkMatchArray = /^https:\/\/yukicoder\.me\/problems\/no\/(\d+)/.exec(lnk.href);
        if (contestSubmissionsPageProblemLnkMatchArray === null) {
            throw new Error('テーブル行内に含まれる問題リンク先が不正です');
        }
        const problemNo = Number(contestSubmissionsPageProblemLnkMatchArray[1]);
        if (!problemNo2ProblemMap.has(problemNo)) {
            try {
                const problem = await APIClient.fetchProblemByNo(problemNo);
                problemNo2ProblemMap.set(problemNo, problem);
            }
            catch (error) {
                problemNo2ProblemMap.set(problemNo, null);
            }
        }
        const problem = problemNo2ProblemMap.get(problemNo);
        if (problem === null || problem === undefined)
            return;
        const label = problemId2Label.get(problem.ProblemId);
        if (label !== undefined)
            lnk.insertAdjacentText('afterbegin', `#${label} `);
        // color authors and testers
        const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
        colorSubmissionRow(row, problem.AuthorId, testerIds, yourId);
    }
};
 
const SUBMISSION_STATUSES = ['AC', 'WA', 'TLE', '--', 'MLE', 'OLE', 'QLE', 'RE', 'CE', 'IE', 'NoOut'];
const stringToStatus = (resultText) => {
    for (let i = 0; i < SUBMISSION_STATUSES.length; ++i) {
        if (SUBMISSION_STATUSES[i] == resultText)
            return SUBMISSION_STATUSES[i];
    }
    throw new Error(`未知のジャッジステータスです: ${resultText}`);
};
const onSubmissionResultPage = async (APIClient) => {
    const toplinksManager = new TopLinksManager();
    const contestInfoCard = new ContestInfoCard();
    const [resultCard, resultCardWrapper] = createCard();
    {
        // count
        const resultCountMap = SUBMISSION_STATUSES.reduce((prevMap, label) => prevMap.set(label, 0), new Map());
        // ジャッジ中(提出直後)は,このテーブルは存在しない
        const testTable = document.getElementById('test_table');
        if (testTable !== null) {
            const results = testTable.querySelectorAll('tbody tr td span.label');
            results.forEach((span) => {
                var _a;
                const resultText = span.textContent;
                if (resultText === null) {
                    throw new Error('ジャッジ結果テキストが空です');
                }
                const resultLabel = stringToStatus(resultText.trim());
                const cnt = (_a = resultCountMap.get(resultLabel)) !== null && _a !== void 0 ? _a : 0;
                resultCountMap.set(resultLabel, cnt + 1);
            });
        }
        const content = document.querySelector('div#testcase_table h4');
        // 提出直後,ジャッジ中は null
        if (content !== null) {
            content.insertAdjacentElement('afterend', resultCardWrapper);
            // print result
            const addResultRow = (cnt, label) => {
                const resultEntry = document.createElement('div');
                const labelSpan = document.createElement('span');
                labelSpan.textContent = label;
                labelSpan.classList.add('label');
                labelSpan.classList.add(label === 'AC' ? 'label-success' : label === 'IE' ? 'label-danger' : 'label-warning');
                resultEntry.appendChild(labelSpan);
                const countSpan = document.createTextNode(` × ${cnt}`);
                resultEntry.appendChild(countSpan);
                resultCard.appendChild(resultEntry);
            };
            resultCountMap.forEach((cnt, label) => {
                if (cnt > 0)
                    addResultRow(cnt, label);
            });
        }
    }
    const lnk = document.querySelector('div#content a[href^="/problems/no/"]');
    if (lnk === null) {
        throw new Error('結果ページ中に問題ページへのリンクが見つかりませんでした');
    }
    toplinksManager.initLink('問題', 'js-cy-problem', lnk.href);
    toplinksManager.initContestLinks();
    const submissionPageProblemLnkMatchArray = /^https:\/\/yukicoder\.me\/problems\/no\/(\d+)/.exec(lnk.href);
    if (submissionPageProblemLnkMatchArray === null) {
        throw new Error('結果ページに含まれる問題ページへのリンク先が不正です');
    }
    // get problems/contests info
    const problemNo = Number(submissionPageProblemLnkMatchArray[1]);
    const [problem, problems, currentContest, pastContest] = await Promise.all([
        APIClient.fetchProblemByNo(problemNo),
        APIClient.fetchProblems(),
        APIClient.fetchCurrentContests(),
        APIClient.fetchPastContests(),
    ]);
    const contests = currentContest.concat(pastContest);
    const contest = contests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
    // add tabs
    if (contest !== undefined) {
        const contestProblems = getContestProblems(contest, problems);
        toplinksManager.confirmContestLinks(contest.Id, contestProblems);
        // print contest info
        contestInfoCard.confirmContestAndProblem(contest, problem);
    }
};
 
const BASE_URL = 'https://yukicoder.me';
const STATIC_API_BASE_URL = `${BASE_URL}/api/v1`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const assertResultIsValid = (obj) => {
    if ('Message' in obj)
        throw new Error(obj.Message);
};
const fetchJson = async (url) => {
    const res = await fetch(url);
    if (!res.ok) {
        throw new Error(res.statusText);
    }
    const obj = (await res.json());
    assertResultIsValid(obj);
    return obj;
};
// TODO pid/no->contest, の変換も受け持つほうが良い?(html 解析絡みをこのクラスに隠蔽できる)
// 「現在のコンテスト」
class CachedAPIClient {
    constructor() {
        this.pastContestsMap = new Map();
        this.currentContestsMap = new Map();
        this.futureContestsMap = new Map();
        this.problemsMapById = new Map();
        this.problemsMapByNo = new Map();
    }
    async fetchPastContests() {
        if (this.pastContests === undefined) {
            this.pastContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/past`);
            this.pastContests.forEach((contest) => {
                if (!this.pastContestsMap.has(contest.Id))
                    this.pastContestsMap.set(contest.Id, contest);
            });
        }
        return this.pastContests;
    }
    async fetchCurrentContests() {
        if (this.currentContests === undefined) {
            this.currentContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/current`);
            this.currentContests.forEach((contest) => {
                if (!this.currentContestsMap.has(contest.Id))
                    this.currentContestsMap.set(contest.Id, contest);
            });
        }
        return this.currentContests;
    }
    async fetchFutureContests() {
        if (this.futureContests === undefined) {
            this.futureContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/future`);
            this.futureContests.forEach((contest) => {
                if (!this.futureContestsMap.has(contest.Id))
                    this.futureContestsMap.set(contest.Id, contest);
            });
        }
        return this.futureContests;
    }
    async fetchContestById(contestId) {
        if (this.pastContestsMap.has(contestId)) {
            return this.pastContestsMap.get(contestId);
        }
        if (this.currentContestsMap.has(contestId)) {
            return this.currentContestsMap.get(contestId);
        }
        if (this.futureContestsMap.has(contestId)) {
            return this.futureContestsMap.get(contestId);
        }
        const contest = await fetchJson(`${STATIC_API_BASE_URL}/contest/id/${contestId}`);
        const currentDate = new Date();
        const startDate = new Date(contest.Date);
        const endDate = new Date(contest.EndDate);
        if (currentDate > endDate) {
            this.pastContestsMap.set(contestId, contest);
        }
        else if (currentDate > startDate) {
            this.currentContestsMap.set(contestId, contest);
        }
        return contest;
    }
    async fetchProblems() {
        if (this.problems === undefined) {
            this.problems = await fetchJson(`${STATIC_API_BASE_URL}/problems`);
            this.problems.forEach((problem) => {
                if (!this.problemsMapById.has(problem.ProblemId))
                    this.problemsMapById.set(problem.ProblemId, problem);
                if (problem.No !== null && !this.problemsMapByNo.has(problem.No))
                    this.problemsMapByNo.set(problem.No, problem);
            });
        }
        return this.problems;
    }
    async fetchProblemById(problemId) {
        if (this.problemsMapById.has(problemId)) {
            return this.problemsMapById.get(problemId);
        }
        try {
            const problem = await fetchJson(`${STATIC_API_BASE_URL}/problems/${problemId}`);
            this.problemsMapById.set(problem.ProblemId, problem);
            if (problem.No !== null)
                this.problemsMapByNo.set(problem.No, problem);
            return problem;
        }
        catch (_a) {
            await this.fetchProblems();
            if (this.problemsMapById.has(problemId)) {
                return this.problemsMapById.get(problemId);
            }
            // 問題一覧には載っていない -> 未来のコンテストの問題
            // ProblemId なので,未来のコンテスト一覧に載っている pid リストから,
            // コンテストは特定可能.
            return { ProblemId: problemId, No: null };
        }
    }
    async fetchProblemByNo(problemNo) {
        if (this.problemsMapByNo.has(problemNo)) {
            return this.problemsMapByNo.get(problemNo);
        }
        try {
            const problem = await fetchJson(`${STATIC_API_BASE_URL}/problems/no/${problemNo}`);
            this.problemsMapById.set(problem.ProblemId, problem);
            if (problem.No !== null)
                this.problemsMapByNo.set(problem.No, problem);
            return problem;
        }
        catch (_a) {
            await this.fetchProblems();
            if (this.problemsMapByNo.has(problemNo)) {
                return this.problemsMapByNo.get(problemNo);
            }
            // 問題一覧には載っていない -> 未来のコンテストの問題
            return { No: problemNo };
        }
    }
}
 
void (async () => {
    const href = location.href;
    const hrefMatchArray = /^https:\/\/yukicoder\.me(.+)/.exec(href);
    if (hrefMatchArray === null)
        return;
    const path = hrefMatchArray[1];
    const APIClient = new CachedAPIClient();
    // on problem page (ProblemNo)
    // e.g. https://yukicoder.me/problems/no/1313
    const problemPageMatchArray = /^\/problems\/no\/(\d+)(.*)/.exec(path);
    if (problemPageMatchArray !== null) {
        // get contest info
        const problemNo = Number(problemPageMatchArray[1]);
        const suffix = problemPageMatchArray[2];
        const problem = await onProblemPageByNo(problemNo, suffix, APIClient);
        if (problem === null)
            return;
        const problemSubmissionsPageMatchArray = /^\/problems\/no\/(\d+)\/submissions/.exec(path);
        if (problemSubmissionsPageMatchArray !== null) {
            onProblemSubmissionsPage(problem);
        }
        // on problem score page (ProblemNo)
        // e.g. https://yukicoder.me/problems/no/5004/score
        const problemScorePageMatchArray = /^\/problems\/no\/(\d+)\/score(.*)/.exec(path);
        if (problemScorePageMatchArray !== null) {
            onProblemScorePage(problem);
        }
        return;
    }
    // on problem page (ProblemId)
    // e.g. https://yukicoder.me/problems/5191
    const problemPageByIdMatchArray = /^\/problems\/(\d+)(.*)/.exec(path);
    if (problemPageByIdMatchArray !== null) {
        // get contest info
        const problemId = Number(problemPageByIdMatchArray[1]);
        const suffix = problemPageByIdMatchArray[2];
        const problem = await onProblemPageById(problemId, suffix, APIClient);
        if (problem === null)
            return;
        const problemSubmissionsPageMatchArray = /^\/problems\/(\d+)\/submissions/.exec(path);
        if (problemSubmissionsPageMatchArray !== null) {
            onProblemSubmissionsPage(problem);
        }
        return;
    }
    // on contest submissions page / statistics page
    // e.g. https://yukicoder.me/contests/300/submissions, https://yukicoder.me/contests/300/statistics
    const contestSubmissionsPageMatchArray = /^\/contests\/(\d+)\/(submissions|statistics)/.exec(path);
    if (contestSubmissionsPageMatchArray !== null) {
        const contestId = Number(contestSubmissionsPageMatchArray[1]);
        await onContestSubmissionsPage(contestId, APIClient);
        return;
    }
    // on submission result page
    // e.g. https://yukicoder.me/submissions/591424
    const submissionPageMatchArray = /^\/submissions\/\d+/.exec(path);
    if (submissionPageMatchArray !== null) {
        await onSubmissionResultPage(APIClient);
        return;
    }
    // on contest leaderboard page
    // e.g. https://yukicoder.me/contests/300/table
    const leaderboardPageMatchArray = /^\/contests\/(\d+)\/(table|all)/.exec(path);
    if (leaderboardPageMatchArray !== null) {
        const contestId = Number(leaderboardPageMatchArray[1]);
        await onLeaderboardPage(contestId, APIClient);
        return;
    }
    // on contest problem list page
    // e.g. https://yukicoder.me/contests/300
    const contestPageMatchArray = /^\/contests\/(\d+)$/.exec(path);
    if (contestPageMatchArray !== null) {
        const contestId = Number(contestPageMatchArray[1]);
        await onContestPage(contestId, APIClient);
        return;
    }
})();