AtCoder Easy Test v2

Make testing sample cases easy

2021-09-29 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name        AtCoder Easy Test v2
// @namespace   https://atcoder.jp/
// @version     2.0.1
// @description Make testing sample cases easy
// @author      magurofly
// @license     MIT
// @supportURL  https://github.com/magurofly/atcoder-easy-test/
// @match       https://atcoder.jp/contests/*/tasks/*
// @grant       unsafeWindow
// ==/UserScript==
(function() {
const codeSaver = {
    LIMIT: 10,
    get() {
        // `json` は、ソースコード文字列またはJSON文字列
        let json = unsafeWindow.localStorage.AtCoderEasyTest$lastCode;
        let data = [];
        try {
            if (typeof json == "string") {
                data.push(...JSON.parse(json));
            }
            else {
                data = [];
            }
        }
        catch (e) {
            data.push({
                path: unsafeWindow.localStorage.AtCoderEasyTset$lastPage,
                code: json,
            });
        }
        return data;
    },
    set(data) {
        unsafeWindow.localStorage.AtCoderEasyTest$lastCode = JSON.stringify(data);
    },
    save(code) {
        let data = codeSaver.get();
        const idx = data.findIndex(({ path }) => path == location.pathname);
        if (idx != -1)
            data.splice(idx, idx + 1);
        data.push({
            path: location.pathname,
            code,
        });
        while (data.length > codeSaver.LIMIT)
            data.shift();
        codeSaver.set(data);
    },
    restore() {
        const data = codeSaver.get();
        const idx = data.findIndex(({ path }) => path == location.pathname);
        if (idx == -1 || !(data[idx] instanceof Object))
            return Promise.reject(`No saved code found for ${location.pathname}`);
        return Promise.resolve(data[idx].code);
    }
};

class CodeRunner {
    get label() {
        return this._label;
    }
    constructor(label, site) {
        this._label = `${label} [${site}]`;
    }
    async test(sourceCode, input, expectedOutput, options) {
        const result = await this.run(sourceCode, input);
        if (expectedOutput != null)
            result.expectedOutput = expectedOutput;
        if (result.status != "OK" || typeof expectedOutput != "string")
            return result;
        let output = result.output || "";
        if (options.trim) {
            expectedOutput = expectedOutput.trim();
            output = output.trim();
        }
        let equals = (x, y) => x === y;
        if (options.allowableError) {
            const floatPattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
            const superEquals = equals;
            equals = (x, y) => {
                if (floatPattern.test(x) && floatPattern.test(y))
                    return Math.abs(parseFloat(x) - parseFloat(y)) <= options.allowableError;
                return superEquals(x, y);
            };
        }
        if (options.split) {
            const superEquals = equals;
            equals = (x, y) => {
                const xs = x.split(/\s+/);
                const ys = y.split(/\s+/);
                if (xs.length != ys.length)
                    return false;
                const len = xs.length;
                for (let i = 0; i < len; i++) {
                    if (!superEquals(xs[i], ys[i]))
                        return false;
                }
                return true;
            };
        }
        result.status = equals(output, expectedOutput) ? "AC" : "WA";
        return result;
    }
}

class CustomRunner extends CodeRunner {
    run;
    constructor(label, run) {
        super(label, "Browser");
        this.run = run;
    }
}

function buildParams(data) {
    return Object.entries(data).map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
}
function sleep(ms) {
    return new Promise(done => setTimeout(done, ms));
}

let waitAtCoderCustomTest = Promise.resolve();
const AtCoderCustomTestBase = location.href.replace(/\/tasks\/.+$/, "/custom_test");
const AtCoderCustomTestResultAPI = AtCoderCustomTestBase + "/json?reload=true";
const AtCoderCustomTestSubmitAPI = AtCoderCustomTestBase + "/submit/json";
class AtCoderRunner extends CodeRunner {
    languageId;
    constructor(languageId, label) {
        super(label, "AtCoder");
        this.languageId = languageId;
    }
    async run(sourceCode, input) {
        const promise = this.submit(sourceCode, input);
        waitAtCoderCustomTest = promise;
        return await promise;
    }
    async submit(sourceCode, input) {
        try {
            await waitAtCoderCustomTest;
        }
        catch (error) {
            console.error(error);
        }
        const error = await fetch(AtCoderCustomTestSubmitAPI, {
            method: "POST",
            credentials: "include",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
            },
            body: buildParams({
                "data.LanguageId": String(this.languageId),
                sourceCode,
                input,
                csrf_token: unsafeWindow.csrfToken,
            }),
        }).then(r => r.text());
        if (error) {
            throw new Error(error);
        }
        await sleep(100);
        for (;;) {
            const data = await fetch(AtCoderCustomTestResultAPI, {
                method: "GET",
                credentials: "include",
            }).then(r => r.json());
            if (!("Result" in data))
                continue;
            const result = data.Result;
            if ("Interval" in data) {
                await sleep(data.Interval);
                continue;
            }
            return {
                status: (result.ExitCode == 0) ? "OK" : (result.TimeConsumption == -1) ? "CE" : "RE",
                exitCode: result.ExitCode,
                execTime: result.TimeConsumption,
                memory: result.MemoryConsumption,
                input,
                output: data.Stdout,
                error: data.Stderr,
            };
        }
    }
}

class PaizaIORunner extends CodeRunner {
    name;
    constructor(name, label) {
        super(label, "PaizaIO");
        this.name = name;
    }
    async run(sourceCode, input) {
        let id, status, error;
        try {
            const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
                source_code: sourceCode,
                language: this.name,
                input,
                longpoll: "true",
                longpoll_timeout: "10",
                api_key: "guest",
            }), {
                method: "POST",
                mode: "cors",
            }).then(r => r.json());
            id = res.id;
            status = res.status;
            error = res.error;
        }
        catch (error) {
            return {
                status: "IE",
                input,
                error: String(error),
            };
        }
        while (status == "running") {
            const res = await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
                id,
                api_key: "guest",
            }), {
                mode: "cors",
            }).then(res => res.json());
            status = res.status;
            error = res.error;
        }
        const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
            id,
            api_key: "guest",
        }), {
            mode: "cors",
        }).then(r => r.json());
        const result = {
            status: "OK",
            exitCode: String(res.exit_code),
            execTime: +res.time * 1e3,
            memory: +res.memory * 1e-3,
            input,
        };
        if (res.build_result == "failure") {
            result.status = "CE";
            result.exitCode = res.build_exit_code;
            result.output = res.build_stdout;
            result.error = res.build_stderr;
        }
        else {
            result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
            result.exitCode = res.exit_code;
            result.output = res.stdout;
            result.error = res.stderr;
        }
        return result;
    }
}

class WandboxRunner extends CodeRunner {
    name;
    options;
    constructor(name, label, options = {}) {
        super(label, "Wandbox");
        this.name = name;
        this.options = options;
    }
    getOptions(sourceCode, input) {
        if (typeof this.options == "function")
            return this.options(sourceCode, input);
        return this.options;
    }
    run(sourceCode, input) {
        const options = this.getOptions(sourceCode, input);
        return this.request(Object.assign({
            compiler: this.name,
            code: sourceCode,
            stdin: input,
        }, options));
    }
    async request(body) {
        const startTime = Date.now();
        let res;
        try {
            res = await fetch("https://wandbox.org/api/compile.json", {
                method: "POST",
                mode: "cors",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(body),
            }).then(r => r.json());
        }
        catch (error) {
            console.error(error);
            return {
                status: "IE",
                input: body.stdin,
                error: String(error),
            };
        }
        const endTime = Date.now();
        const result = {
            status: "OK",
            exitCode: String(res.status),
            execTime: endTime - startTime,
            input: body.stdin,
            output: String(res.program_output || ""),
            error: String(res.program_error || ""),
        };
        // 正常終了以外の場合
        if (res.status != 0) {
            if (res.signal) {
                result.exitCode += ` (${res.signal})`;
            }
            result.output = String(res.compiler_output || "") + String(result.output || "");
            result.error = String(res.compiler_error || "") + String(result.error || "");
            if (res.compiler_output || res.compiler_error) {
                result.status = "CE";
            }
            else {
                result.status = "RE";
            }
        }
        return result;
    }
}

class WandboxCppRunner extends WandboxRunner {
    async run(sourceCode, input) {
        // ACL を結合する
        const ACLBase = "https://cdn.jsdelivr.net/gh/atcoder/ac-library/";
        const files = new Map();
        const includeHeader = async (source) => {
            const pattern = /^#\s*include\s*[<"]atcoder\/([^>"]+)[>"]/gm;
            const loaded = [];
            let match;
            while (match = pattern.exec(source)) {
                const file = "atcoder/" + match[1];
                if (files.has(file))
                    continue;
                files.set(file, null);
                loaded.push([file, fetch(ACLBase + file, { mode: "cors", cache: "force-cache", }).then(r => r.text())]);
            }
            const included = await Promise.all(loaded.map(async ([file, r]) => {
                const source = await r;
                files.set(file, source);
                return source;
            }));
            for (const source of included) {
                await includeHeader(source);
            }
        };
        await includeHeader(sourceCode);
        const codes = [];
        for (const [file, code] of files) {
            codes.push({ file, code, });
        }
        const options = this.getOptions(sourceCode, input);
        return await this.request(Object.assign({
            compiler: this.name,
            code: sourceCode,
            stdin: input,
            codes,
            "compiler-option-raw": "-I.",
        }, options));
    }
}

let brythonRunnerLoaded = false;
const brythonRunner = new CustomRunner("Brython", async (sourceCode, input) => {
    if (!brythonRunnerLoaded) {
        // BrythonRunner を読み込む
        await new Promise((resolve) => {
            const script = document.createElement("script");
            script.src = "https://cdn.jsdelivr.net/gh/pythonpad/brython-runner/lib/brython-runner.bundle.js";
            script.onload = () => {
                brythonRunnerLoaded = true;
                resolve(null);
            };
            document.head.appendChild(script);
        });
    }
    let stdout = "";
    let stderr = "";
    let stdinOffset = 0;
    const BrythonRunner = unsafeWindow.BrythonRunner;
    const runner = new BrythonRunner({
        stdout: { write(content) { stdout += content; }, flush() { } },
        stderr: { write(content) { stderr += content; }, flush() { } },
        stdin: { async readline() {
                let index = input.indexOf("\n", stdinOffset) + 1;
                if (index == 0)
                    index = input.length;
                const text = input.slice(stdinOffset, index);
                stdinOffset = index;
                return text;
            } },
    });
    const timeStart = Date.now();
    await runner.runCode(sourceCode);
    const timeEnd = Date.now();
    return {
        status: "OK",
        exitCode: "0",
        execTime: (timeEnd - timeStart),
        input,
        output: stdout,
        error: stderr,
    };
});

const runners = {
    "4001": [new WandboxRunner("gcc-10.1.0-c", "C (GCC 10.1.0)")],
    "4002": [new PaizaIORunner("c", "C (C17 / Clang 10.0.0)")],
    "4003": [new WandboxCppRunner("gcc-10.1.0", "C++ (GCC 10.1.0)", { options: "warning,boost-1.73.0-gcc-9.2.0,gnu++17" })],
    "4004": [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)", { options: "warning,boost-nothing-clang-10.0.0,c++17" })],
    "4006": [
        new PaizaIORunner("python3", "Python (3.8.2)"),
        brythonRunner,
    ],
    "4007": [new PaizaIORunner("bash", "Bash (5.0.17)")],
    "4010": [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
    "4011": [new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)")],
    "4013": [new PaizaIORunner("clojure", "Clojure (1.10.1-1)")],
    "4017": [new PaizaIORunner("d", "D (LDC 1.23.0)")],
    "4020": [new PaizaIORunner("erlang", "Erlang (10.6.4)")],
    "4021": [new PaizaIORunner("elixir", "Elixir (1.10.4)")],
    "4022": [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
    "4023": [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
    "4026": [new WandboxRunner("go-1.14.1", "Go (1.14.1)")],
    "4027": [new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)")],
    "4030": [new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)")],
    "4032": [new PaizaIORunner("kotlin", "Kotlin (1.4.0)")],
    "4033": [new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)")],
    "4034": [new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)")],
    "4036": [new WandboxRunner("nim-1.0.6", "Nim (1.0.6)")],
    "4037": [new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)")],
    "4039": [new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)")],
    "4041": [new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)")],
    "4042": [new PaizaIORunner("perl", "Perl (5.30.0)")],
    "4044": [
        new PaizaIORunner("php", "PHP (7.4.10)"),
        new WandboxRunner("php-7.3.3", "PHP (7.3.3)"),
    ],
    "4046": [new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)")],
    "4047": [new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)")],
    "4049": [
        new PaizaIORunner("ruby", "Ruby (2.7.1)"),
        new WandboxRunner("ruby-head", "Ruby (HEAD 3.0.0dev)"),
        new WandboxRunner("ruby-2.7.0-preview1", "Ruby (2.7.0-preview1)"),
    ],
    "4050": [
        new AtCoderRunner("4050", "Rust (1.42.0)"),
        new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
        new PaizaIORunner("rust", "Rust (1.43.0)"),
    ],
    "4051": [new PaizaIORunner("scala", "Scala (2.13.3)")],
    "4053": [new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)")],
    "4055": [new PaizaIORunner("swift", "Swift (5.2.5)")],
    "4056": [new CustomRunner("Text", async (sourceCode, input) => {
            return {
                status: "OK",
                exitCode: "0",
                input,
                output: sourceCode,
            };
        })],
    "4058": [new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)")],
    "4061": [new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)")],
    "4101": [new WandboxCppRunner("gcc-9.2.0", "C++ (GCC 9.2.0)")],
    "4102": [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)")],
};
for (const e of document.querySelectorAll("#select-lang option[value]")) {
    const languageId = e.value;
    // 特別な CodeRunner が定義されていない言語ID
    if (!(languageId in runners))
        runners[languageId] = [];
    // AtCoderRunner がない場合は、追加する
    if (runners[languageId].some((runner) => runner instanceof AtCoderRunner))
        continue;
    runners[languageId].push(new AtCoderRunner(languageId, e.textContent));
}
console.info("AtCoder Easy Test: codeRunner OK");
var codeRunner = {
    // 指定した環境でコードを実行する
    run(languageId, index, sourceCode, input, expectedOutput, options = { trim: true, split: true }) {
        // CodeRunner が存在しない言語ID
        if (!(languageId in runners))
            return Promise.reject("Language not supported");
        if (!(index in runners[languageId]))
            return Promise.reject(`Runner index out of range: [0, ${runners[languageId].length})`);
        // 最後に実行したコードを保存
        codeSaver.save(sourceCode);
        // 実行
        return runners[languageId][index].test(sourceCode, input, expectedOutput, options);
    },
    // 環境の名前の一覧を取得する
    async getEnvironment(languageId) {
        if (!(languageId in runners))
            throw "language not supported";
        return runners[languageId].map((runner) => runner.label);
    },
};

function getTestCases() {
    const selectors = [
        ["#task-statement p+pre.literal-block", ".section"],
        ["#task-statement pre.source-code-for-copy", ".part"],
        ["#task-statement .lang>*:nth-child(1) .div-btn-copy+pre", ".part"],
        ["#task-statement .div-btn-copy+pre", ".part"],
        ["#task-statement>.part pre.linenums", ".part"],
        ["#task-statement>.part:not(.io-style)>h3+section>pre", ".part"],
        ["#task-statement pre", ".part"],
    ];
    for (const [selector, closestSelector] of selectors) {
        const e = [...document.querySelectorAll(selector)].filter(e => {
            if (e.closest(".io-style"))
                return false; // practice2
            return true;
        });
        if (e.length == 0)
            continue;
        const testcases = [];
        let sampleId = 1;
        for (let i = 0; i < e.length; i += 2) {
            const container = e[i].closest(closestSelector) || e[i].parentElement;
            testcases.push({
                title: `Sample ${sampleId++}`,
                input: (e[i] || {}).textContent,
                output: (e[i + 1] || {}).textContent,
                anchor: container.querySelector("h3"),
            });
        }
        return testcases;
    }
    return [];
}

function html2element(html) {
    const template = document.createElement("template");
    template.innerHTML = html;
    return template.content.firstChild;
}
const eventListeners = {};
const events = {
    on(name, listener) {
        const listeners = (name in eventListeners ? eventListeners[name] : eventListeners[name] = []);
        listeners.push(listener);
    },
    trig(name) {
        if (name in eventListeners) {
            for (const listener of eventListeners[name])
                listener();
        }
    },
};

var hBottomMenu = "<div id=\"bottom-menu-wrapper\" class=\"navbar navbar-default navbar-fixed-bottom\">\n  <div class=\"container\">\n    <div class=\"navbar-header\">\n      <button id=\"bottom-menu-key\" type=\"button\" class=\"navbar-toggle collapsed glyphicon glyphicon-menu-down\" data-toggle=\"collapse\" data-target=\"#bottom-menu\"></button>\n    </div>\n    <div id=\"bottom-menu\" class=\"collapse navbar-collapse\">\n      <ul id=\"bottom-menu-tabs\" class=\"nav nav-tabs\"></ul>\n      <div id=\"bottom-menu-contents\" class=\"tab-content\"></div>\n    </div>\n  </div>\n</div>";

var hStyle$1 = "<style>\n#bottom-menu-wrapper {\n  background: transparent;\n  border: none;\n  pointer-events: none;\n  padding: 0;\n}\n\n#bottom-menu-wrapper>.container {\n  position: absolute;\n  bottom: 0;\n  width: 100%;\n  padding: 0;\n}\n\n#bottom-menu-wrapper>.container>.navbar-header {\n  float: none;\n}\n\n#bottom-menu-key {\n  display: block;\n  float: none;\n  margin: 0 auto;\n  padding: 10px 3em;\n  border-radius: 5px 5px 0 0;\n  background: #000;\n  opacity: 0.5;\n  color: #FFF;\n  cursor: pointer;\n  pointer-events: auto;\n  text-align: center;\n}\n\n@media screen and (max-width: 767px) {\n  #bottom-menu-key {\n    opacity: 0.25;\n  }\n}\n\n#bottom-menu-key.collapsed:before {\n  content: \"\\e260\";\n}\n\n#bottom-menu-tabs {\n  padding: 3px 0 0 10px;\n  cursor: n-resize;\n}\n\n#bottom-menu-tabs a {\n  pointer-events: auto;\n}\n\n#bottom-menu {\n  pointer-events: auto;\n  background: rgba(0, 0, 0, 0.8);\n  color: #fff;\n  max-height: unset;\n}\n\n#bottom-menu.collapse:not(.in) {\n  display: none !important;\n}\n\n#bottom-menu-tabs>li>a {\n  background: rgba(150, 150, 150, 0.5);\n  color: #000;\n  border: solid 1px #ccc;\n  filter: brightness(0.75);\n}\n\n#bottom-menu-tabs>li>a:hover {\n  background: rgba(150, 150, 150, 0.5);\n  border: solid 1px #ccc;\n  color: #111;\n  filter: brightness(0.9);\n}\n\n#bottom-menu-tabs>li.active>a {\n  background: #eee;\n  border: solid 1px #ccc;\n  color: #333;\n  filter: none;\n}\n\n.bottom-menu-btn-close {\n  font-size: 8pt;\n  vertical-align: baseline;\n  padding: 0 0 0 6px;\n  margin-right: -6px;\n}\n\n#bottom-menu-contents {\n  padding: 5px 15px;\n  max-height: 50vh;\n  overflow-y: auto;\n}\n\n#bottom-menu-contents .panel {\n  color: #333;\n}\n</style>";

const style = html2element(hStyle$1);
const bottomMenu = html2element(hBottomMenu);
unsafeWindow.document.head.appendChild(style);
unsafeWindow.document.getElementById("main-div").appendChild(bottomMenu);
const bottomMenuKey = bottomMenu.querySelector("#bottom-menu-key");
const bottomMenuTabs = bottomMenu.querySelector("#bottom-menu-tabs");
const bottomMenuContents = bottomMenu.querySelector("#bottom-menu-contents");
// メニューのリサイズ
{
    let resizeStart = null;
    const onStart = (event) => {
        const target = event.target;
        const pageY = event.pageY;
        if (target.id != "bottom-menu-tabs")
            return;
        resizeStart = { y: pageY, height: bottomMenuContents.getBoundingClientRect().height };
    };
    const onMove = (event) => {
        if (!resizeStart)
            return;
        event.preventDefault();
        bottomMenuContents.style.height = `${resizeStart.height - (event.pageY - resizeStart.y)}px`;
    };
    const onEnd = () => {
        resizeStart = null;
    };
    bottomMenuTabs.addEventListener("mousedown", onStart);
    bottomMenuTabs.addEventListener("mousemove", onMove);
    bottomMenuTabs.addEventListener("mouseup", onEnd);
    bottomMenuTabs.addEventListener("mouseleave", onEnd);
}
let tabs = new Set();
let selectedTab = null;
/** 下メニューの操作 */
const menuController = {
    /** タブを選択 */
    selectTab(tabId) {
        const tab = unsafeWindow.$(`#bottom-menu-tab-${tabId}`);
        if (tab && tab[0]) {
            tab.tab("show"); // Bootstrap 3
            selectedTab = tabId;
        }
    },
    /** 下メニューにタブを追加する */
    addTab(tabId, tabLabel, paneContent, options = {}) {
        console.log(`AtCoder Easy Test: addTab: ${tabLabel} (${tabId})`, paneContent);
        // タブを追加
        const tab = document.createElement("a");
        tab.textContent = tabLabel;
        tab.id = `bottom-menu-tab-${tabId}`;
        tab.href = "#";
        tab.dataset.target = `#bottom-menu-pane-${tabId}`;
        tab.dataset.toggle = "tab";
        tab.addEventListener("click", event => {
            event.preventDefault();
            menuController.selectTab(tabId);
        });
        const tabLi = document.createElement("li");
        tabLi.appendChild(tab);
        bottomMenuTabs.appendChild(tabLi);
        // 内容を追加
        const pane = document.createElement("div");
        pane.className = "tab-pane";
        pane.id = `bottom-menu-pane-${tabId}`;
        pane.appendChild(paneContent);
        bottomMenuContents.appendChild(pane);
        const controller = {
            get id() {
                return tabId;
            },
            close() {
                bottomMenuTabs.removeChild(tabLi);
                bottomMenuContents.removeChild(pane);
                tabs.delete(tab);
                if (selectedTab == tabId) {
                    selectedTab = null;
                    if (tabs.size > 0) {
                        menuController.selectTab(tabs.values().next().value.id);
                    }
                }
            },
            show() {
                menuController.show();
                menuController.selectTab(tabId);
            },
            set color(color) {
                tab.style.backgroundColor = color;
            },
        };
        // 選択されているタブがなければ選択
        if (!selectedTab)
            menuController.selectTab(tabId);
        return controller;
    },
    /** 下メニューを表示する */
    show() {
        if (bottomMenuKey.classList.contains("collapsed"))
            bottomMenuKey.click();
    },
    /** 下メニューの表示/非表示を切り替える */
    toggle() {
        bottomMenuKey.click();
    },
};
console.info("AtCoder Easy Test: bottomMenu OK");

var hRowTemplate = "<div class=\"atcoder-easy-test-cases-row alert alert-dismissible\">\n  <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"close\">\n    <span aria-hidden=\"true\">×</span>\n  </button>\n  <div class=\"progress\">\n    <div class=\"progress-bar\" style=\"width: 0%;\">0 / 0</div>\n  </div>\n  <!--div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div>\n  <div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div>\n  <div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div-->\n</div>";

class ResultRow {
    _tabs;
    _element;
    _promise;
    constructor(pairs) {
        this._tabs = pairs.map(([_, tab]) => tab);
        this._element = html2element(hRowTemplate);
        const numCases = pairs.length;
        let numFinished = 0;
        let numAccepted = 0;
        const progressBar = this._element.querySelector(".progress-bar");
        progressBar.textContent = `${numFinished} / ${numCases}`;
        this._promise = Promise.all(pairs.map(([pResult, tab]) => {
            const button = html2element(`<div class="label label-default" style="margin: 3px; cursor: pointer;">WJ</div>`);
            button.addEventListener("click", () => {
                tab.show();
            });
            this._element.appendChild(button);
            return pResult.then(result => {
                button.textContent = result.status;
                if (result.status == "AC") {
                    button.classList.add("label-success");
                }
                else if (result.status != "OK") {
                    button.classList.add("label-warning");
                }
                numFinished++;
                if (result.status == "AC")
                    numAccepted++;
                progressBar.textContent = `${numFinished} / ${numCases}`;
                progressBar.style.width = `${100 * numFinished / numCases}%`;
                if (numFinished == numCases) {
                    if (numAccepted == numCases)
                        this._element.classList.add("alert-success");
                    else
                        this._element.classList.add("alert-warning");
                }
            }).catch(reason => {
                button.textContent = "IE";
                button.classList.add("label-danger");
                console.error(reason);
            });
        }));
    }
    get element() {
        return this._element;
    }
    onFinish(listener) {
        this._promise.then(listener);
    }
    remove() {
        for (const tab of this._tabs)
            tab.close();
        const parent = this._element.parentElement;
        if (parent)
            parent.removeChild(this._element);
    }
}

var hResultList = "<div class=\"row\"></div>";

const eResultList = html2element(hResultList);
unsafeWindow.document.querySelector(".form-code-submit").appendChild(eResultList);
const resultList = {
    addResult(pairs) {
        const result = new ResultRow(pairs);
        eResultList.appendChild(result.element);
        return result;
    },
};

var hTabTemplate = "<div class=\"atcoder-easy-test-result container\">\n  <div class=\"row\">\n    <div class=\"atcoder-easy-test-result-col-input col-xs-12\" data-if-expected-output=\"col-sm-6 col-sm-push-6\">\n      <div class=\"form-group\">\n        <label class=\"control-label col-xs-12\">\n          Standard Input\n          <div class=\"col-xs-12\">\n            <textarea class=\"atcoder-easy-test-result-input form-control\" rows=\"3\" readonly=\"readonly\"></textarea>\n          </div>\n        </label>\n      </div>\n    </div>\n    <div class=\"atcoder-easy-test-result-col-expected-output col-xs-12 col-sm-6 hidden\" data-if-expected-output=\"!hidden col-sm-pull-6\">\n      <div class=\"form-group\">\n        <label class=\"control-label col-xs-12\">\n          Expected Output\n          <div class=\"col-xs-12\">\n            <textarea class=\"atcoder-easy-test-result-expected-output form-control\" rows=\"3\" readonly=\"readonly\"></textarea>\n          </div>\n        </label>\n      </div>\n    </div>\n  </div>\n  <div class=\"row\"><div class=\"col-sm-6 col-sm-offset-3\">\n    <div class=\"panel panel-default\">\n      <table class=\"table table-condensed\">\n        <tbody>\n          <tr>\n            <th class=\"text-center\">Exit Code</th>\n            <th class=\"text-center\">Exec Time</th>\n            <th class=\"text-center\">Memory</th>\n          </tr>\n          <tr>\n            <td class=\"atcoder-easy-test-result-exit-code text-center\"></td>\n            <td class=\"atcoder-easy-test-result-exec-time text-center\"></td>\n            <td class=\"atcoder-easy-test-result-memory text-center\"></td>\n          </tr>\n        </tbody>\n      </table>\n    </div>\n  </div></div>\n  <div class=\"row\">\n    <div class=\"atcoder-easy-test-result-col-output col-xs-12\" data-if-error=\"col-md-6\">\n      <div class=\"form-group\">\n        <label class=\"control-label col-xs-12\">\n          Standard Output\n          <div class=\"col-xs-12\">\n            <textarea class=\"atcoder-easy-test-result-output form-control\" rows=\"5\" readonly=\"readonly\"></textarea>\n          </div>\n        </label>\n      </div>\n    </div>\n    <div class=\"atcoder-easy-test-result-col-error col-xs-12 col-md-6 hidden\" data-if-error=\"!hidden\">\n      <div class=\"form-group\">\n        <label class=\"control-label col-xs-12\">\n          Standard Error\n          <div class=\"col-xs-12\">\n            <textarea class=\"atcoder-easy-test-result-error form-control\" rows=\"5\" readonly=\"readonly\"></textarea>\n          </div>\n        </label>\n      </div>\n    </div>\n  </div>\n</div>";

function setClassFromData(element, name) {
    const classes = element.dataset[name].split(/\s+/);
    for (let className of classes) {
        let flag = true;
        if (className[0] == "!") {
            className = className.slice(1);
            flag = false;
        }
        element.classList.toggle(className, flag);
    }
}
class ResultTabContent {
    _title;
    _uid;
    _element;
    _result;
    constructor() {
        this._uid = Date.now().toString(16);
        this._result = null;
        this._element = html2element(hTabTemplate);
        this._element.id = `atcoder-easy-test-result-${this._uid}`;
    }
    set result(result) {
        this._result = result;
        if (result.status == "AC") {
            this.outputStyle.backgroundColor = "#dff0d8";
        }
        else if (result.status != "OK") {
            this.outputStyle.backgroundColor = "#fcf8e3";
        }
        this.input = result.input;
        if ("expectedOutput" in result)
            this.expectedOutput = result.expectedOutput;
        this.exitCode = result.exitCode;
        if ("execTime" in result)
            this.execTime = `${result.execTime} ms`;
        if ("memory" in result)
            this.memory = `${result.memory} KB`;
        if ("output" in result)
            this.output = result.output;
        if (result.error)
            this.error = result.error;
    }
    get result() {
        return this._result;
    }
    get uid() {
        return this._uid;
    }
    get element() {
        return this._element;
    }
    set title(title) {
        this._title = title;
    }
    get title() {
        return this._title;
    }
    set input(input) {
        this._get("input").value = input;
    }
    get inputStyle() {
        return this._get("input").style;
    }
    set expectedOutput(output) {
        this._get("expected-output").value = output;
        setClassFromData(this._get("col-input"), "ifExpectedOutput");
        setClassFromData(this._get("col-expected-output"), "ifExpectedOutput");
    }
    get expectedOutputStyle() {
        return this._get("expected-output").style;
    }
    set output(output) {
        this._get("output").value = output;
    }
    get outputStyle() {
        return this._get("output").style;
    }
    set error(error) {
        this._get("error").value = error;
        setClassFromData(this._get("col-output"), "ifError");
        setClassFromData(this._get("col-error"), "ifError");
    }
    set exitCode(code) {
        const element = this._get("exit-code");
        element.textContent = code;
        const isSuccess = code == "0";
        element.classList.toggle("bg-success", isSuccess);
        element.classList.toggle("bg-danger", !isSuccess);
    }
    set execTime(time) {
        this._get("exec-time").textContent = time;
    }
    set memory(memory) {
        this._get("memory").textContent = memory;
    }
    _get(name) {
        return this._element.querySelector(`.atcoder-easy-test-result-${name}`);
    }
}

var hRoot = "<form id=\"atcoder-easy-test-container\" class=\"form-horizontal\">\n  <small style=\"position: absolute; display: block; bottom: 0; right: 0; padding: 1% 4%; width: 95%; text-align: right;\">AtCoder Easy Test v<span id=\"atcoder-easy-test-version\"></span></small>\n  <div class=\"row\">\n      <div class=\"col-xs-12 col-lg-8\">\n          <div class=\"form-group\">\n              <label class=\"control-label col-sm-2\">Test Environment</label>\n              <div class=\"col-sm-10\">\n                  <select class=\"form-control\" id=\"atcoder-easy-test-language\"></select>\n              </div>\n          </div>\n          <div class=\"form-group\">\n              <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-input\">Standard Input</label>\n              <div class=\"col-sm-10\">\n                  <textarea id=\"atcoder-easy-test-input\" name=\"input\" class=\"form-control\" rows=\"3\"></textarea>\n              </div>\n          </div>\n      </div>\n      <div class=\"col-xs-12 col-lg-4\">\n          <details close>\n              <summary>Expected Output</summary>\n              <div class=\"form-group\">\n                  <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-allowable-error-check\">Allowable Error</label>\n                  <div class=\"col-sm-10\">\n                      <div class=\"input-group\">\n                          <span class=\"input-group-addon\">\n                              <input id=\"atcoder-easy-test-allowable-error-check\" type=\"checkbox\" checked=\"checked\">\n                          </span>\n                          <input id=\"atcoder-easy-test-allowable-error\" type=\"text\" class=\"form-control\" value=\"1e-6\">\n                      </div>\n                  </div>\n              </div>\n              <div class=\"form-group\">\n                  <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-output\">Expected Output</label>\n                  <div class=\"col-sm-10\">\n                      <textarea id=\"atcoder-easy-test-output\" name=\"output\" class=\"form-control\" rows=\"3\"></textarea>\n                  </div>\n              </div>\n          </details>\n      </div>\n      <div class=\"col-xs-12\">\n          <div class=\"col-xs-11 col-xs-offset=1\">\n              <div class=\"form-group\">\n                  <a id=\"atcoder-easy-test-run\" class=\"btn btn-primary\">Run</a>\n              </div>\n          </div>\n      </div>\n  </div>\n  <style>\n  #atcoder-easy-test-language {\n      border: none;\n      background: transparent;\n      font: inherit;\n      color: #fff;\n  }\n  #atcoder-easy-test-language option {\n      border: none;\n      color: #333;\n      font: inherit;\n  }\n  </style>\n</form>";

var hStyle = "<style>\n.atcoder-easy-test-result textarea {\n  font-family: monospace;\n  font-weight: normal;\n}\n</style>";

var hTestAndSubmit = "<a id=\"atcoder-easy-test-btn-test-and-submit\" class=\"btn btn-info btn\" style=\"margin-left: 1rem\" title=\"Ctrl+Enter\" data-toggle=\"tooltip\">Test &amp; Submit</a>";

var hTestAllSamples = "<a id=\"atcoder-easy-test-btn-test-all\" class=\"btn btn-default btn-sm\" style=\"margin-left: 1rem\" title=\"Alt+Enter\" data-toggle=\"tooltip\">Test All Samples</a>";

const doc = unsafeWindow.document;
const $ = unsafeWindow.$;
const $select = (selector) => doc.querySelector(selector);
// external interfaces
unsafeWindow.bottomMenu = menuController;
unsafeWindow.codeRunner = codeRunner;
doc.head.appendChild(html2element(hStyle));
// place "Easy Test" tab
{
    const eAtCoderLang = $select("#select-lang>select");
    const eSubmitButton = doc.getElementById("submit");
    // declare const hRoot: string;
    const root = html2element(hRoot);
    const E = (id) => root.querySelector(`#atcoder-easy-test-${id}`);
    const eLanguage = E("language");
    const eInput = E("input");
    const eAllowableErrorCheck = E("allowable-error-check");
    const eAllowableError = E("allowable-error");
    const eOutput = E("output");
    const eRun = E("run");
    E("version").textContent = "2.0.1";
    events.on("enable", () => {
        eRun.classList.remove("disabled");
    });
    events.on("disable", () => {
        eRun.classList.remove("enabled");
    });
    // 言語選択関係
    {
        async function setLanguage() {
            const languageId = eAtCoderLang.value;
            while (eLanguage.firstChild)
                eLanguage.removeChild(eLanguage.firstChild);
            try {
                const labels = await codeRunner.getEnvironment(languageId);
                console.log(`language: ${labels[0]} (${languageId})`);
                labels.forEach((label, index) => {
                    const option = document.createElement("option");
                    option.value = String(index);
                    option.textContent = label;
                    eLanguage.appendChild(option);
                });
                events.trig("enable");
            }
            catch (error) {
                console.log(`language: ? (${languageId})`);
                console.error(error);
                const option = document.createElement("option");
                option.className = "fg-danger";
                option.textContent = error;
                eLanguage.appendChild(option);
                events.trig("disable");
            }
        }
        unsafeWindow.$(eAtCoderLang).change(() => setLanguage()); //NOTE: This event is only for jQuery; do not replace with Vanilla
        eAllowableError.disabled = !eAllowableErrorCheck.checked;
        eAllowableErrorCheck.addEventListener("change", event => {
            eAllowableError.disabled = !eAllowableErrorCheck.checked;
        });
        setLanguage();
    }
    let runId = 0;
    // テスト実行
    function runTest(title, input, output = null) {
        runId++;
        events.trig("disable");
        const options = { trim: true, split: true, };
        if (eAllowableErrorCheck.checked) {
            options.allowableError = parseFloat(eAllowableError.value);
        }
        const content = new ResultTabContent();
        const tab = menuController.addTab("easy-test-result-" + content.uid, `#${runId} ${title}`, content.element, { active: true, closeButton: true });
        const pResult = codeRunner.run(eAtCoderLang.value, +eLanguage.value, unsafeWindow.getSourceCode(), input, output, options);
        pResult.then(result => {
            content.result = result;
            if (result.status == "AC") {
                tab.color = "#dff0d8";
            }
            else if (result.status != "OK") {
                tab.color = "#fcf8e3";
            }
            events.trig("enable");
        });
        return [pResult, tab];
    }
    function runAllCases(testcases) {
        const pairs = testcases.map(testcase => runTest(testcase.title, testcase.input, testcase.output));
        resultList.addResult(pairs);
        return Promise.all(pairs.map(([pResult, _]) => pResult.then(result => {
            if (result.status == "AC")
                return Promise.resolve(result);
            else
                return Promise.reject(result);
        })));
    }
    eRun.addEventListener("click", _ => {
        const title = "Run";
        const input = eInput.value;
        const output = eOutput.value;
        runTest(title, input, output || null);
    });
    menuController.addTab("easy-test", "Easy Test", root);
    // place "Test & Submit" button
    {
        const button = html2element(hTestAndSubmit);
        eSubmitButton.parentElement.appendChild(button);
        button.addEventListener("click", async () => {
            await runAllCases(getTestCases());
            eSubmitButton.click();
        });
    }
    // place "Test All Samples" button
    {
        const button = html2element(hTestAllSamples);
        eSubmitButton.parentElement.appendChild(button);
        button.addEventListener("click", () => runAllCases(getTestCases()));
    }
}
// place "Restore Last Play" button
try {
    const restoreButton = doc.createElement("a");
    restoreButton.className = "btn btn-danger btn-sm";
    restoreButton.textContent = "Restore Last Play";
    restoreButton.addEventListener("click", async () => {
        try {
            const lastCode = await codeSaver.restore();
            if (confirm("Your current code will be replaced. Are you sure?")) {
                $select(".plain-textarea").value = lastCode;
                $(".editor").data("editor").doc.setValue(lastCode);
            }
        }
        catch (reason) {
            alert(reason);
        }
    });
    $select(".editor-buttons").appendChild(restoreButton);
}
catch (e) {
    console.error(e);
}
})();