AtCoder Comfortable Editor

AtCoderのコードテスト・提出欄・提出コードを快適にします

Versión del día 27/5/2022. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name    AtCoder Comfortable Editor
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description    AtCoderのコードテスト・提出欄・提出コードを快適にします
// @author    Chippppp
// @license    MIT
// @match    https://atcoder.jp/contests/*/custom_test*
// @match    https://atcoder.jp/contests/*/submit*
// @match    https://atcoder.jp/contests/*/tasks/*
// @match    https://atcoder.jp/contests/*/submissions/*
// @grant    GM_getValue
// @grant    GM_setValue
// ==/UserScript==

(function() {
    "use strict";

    // requireJS in cdnjs
    let requireJS = document.createElement("script");
    requireJS.src = "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js";
    document.head.prepend(requireJS);

    // Ace Editor in cdnjs
    // Copyright (c) 2010, Ajax.org B.V.
    let aceEditor = document.createElement("script");
    aceEditor.src = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1/ace.js";
    document.head.prepend(aceEditor);

    let isReadOnly = window.location.pathname.indexOf("submissions") != -1;
    let isCustomTest = window.location.pathname.indexOf("custom_test") != -1;

    // 見た目変更
    if (isReadOnly) {
        document.getElementsByClassName("linenums")[0].style.display = "none";
        document.getElementsByClassName("btn-copy btn-pre")[0].style.zIndex = "7";
        document.getElementsByClassName("btn-copy btn-pre")[1].style.zIndex = "7";
        document.getElementsByClassName("btn-copy btn-pre")[0].style.borderRadius = "0";
        document.getElementsByClassName("btn-copy btn-pre")[1].style.borderRadius = "0";
    } else {
        document.getElementsByClassName("btn btn-default btn-sm btn-toggle-editor")[0].style.display = "none";
        document.getElementsByClassName("btn btn-default btn-sm btn-toggle-editor")[0].classList.remove("active");
    }

    // エディタ
    let originalDiv;
    let newDiv = document.createElement("div");
    newDiv.id = "new-div";
    newDiv.style.marginTop = "10px";
    newDiv.style.marginBottom = "10px";
    let originalEditor;
    let newEditor;
    let syncEditor;
    if (isReadOnly) {
        document.getElementById("submission-code").after(newDiv);
    } else {
        originalDiv = document.getElementsByClassName("div-editor")[0];
        originalDiv.style.display = "none";
        document.getElementsByClassName("form-control plain-textarea")[0].style.display = "none";
        originalEditor = $(".editor").data("editor").doc;
        originalDiv.after(newDiv);
        syncEditor = function() {
            code = newEditor.getValue();
            originalEditor.setValue(newEditor.getValue());
        };
    }

    // ボタン
    let languageButton;
    let settingsButton;
    languageButton = document.getElementsByClassName("select2-selection select2-selection--single")[1];
    if (languageButton == undefined) languageButton = document.getElementsByClassName("select2-selection select2-selection--single")[0];
    settingsButton = document.createElement("button");
    newDiv.after(settingsButton);
    settingsButton.className = "btn btn-secondary btn-sm";
    settingsButton.type = "button";
    settingsButton.innerText = "Editor Settings";
    if (!isReadOnly && !isCustomTest) {
        let copyP = document.createElement("p");
        document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].parentElement.after(copyP);
        let copyButton = document.createElement("button");
        copyP.appendChild(copyButton);
        copyButton.className = "btn btn-info btn-sm";
        copyButton.type = "button";
        copyButton.innerText = "Copy From Code Test";
        copyButton.addEventListener("click", function() {
            let href = location.href;
            if (href.indexOf("tasks") != -1) href = href.slice(0, href.indexOf("tasks"));
            else href = href.slice(0, href.indexOf("submit"));
            href += "custom_test";
            fetch(href).then(response => response.text()).then(function(data) {
                const parser = new DOMParser();
                const doc = parser.parseFromString(data, "text/html");
                newEditor.setValue(doc.getElementsByClassName("editor")[0].value, 1);
            });
        });
    }

    // 保存されたコード
    let code;
    if (isCustomTest) {
        code = originalEditor.getValue();
        // ページを去るときに警告
        if (isCustomTest) {
            window.addEventListener("beforeunload", function(e) {
                if (newEditor.getValue() != code) e.returnValue = "The code is not saved, are you sure you want to leave the page?";
            });
        }
    }

    // ボタンでエディターを同期
    if (!isReadOnly) {
        let buttons = Array.from(Array.from(document.getElementsByClassName("col-sm-5")).slice(-1)[0].children);
        for (let originalButton of buttons) {
            let newButton = originalButton.cloneNode(true);
            originalButton.after(newButton);
            originalButton.id = "";
            originalButton.style.display = "none";
            newButton.addEventListener("click", function(e) {
                e.preventDefault();
                syncEditor();
                originalButton.click();
            });
        }
        if (isCustomTest) {
            let submit = vueCustomTest.submit;
            vueCustomTest.submit = function() {
                syncEditor();
                submit();
            };
        }
    }

    // ファイルを開く場合
    if (!isReadOnly) {
        document.getElementById("input-open-file").addEventListener("change", function(e) {
            let fileData = e.target.files[0];
            let reader = new FileReader();
            reader.addEventListener("load", function() {
                newEditor.setValue(reader.result);
            });
            reader.readAsText(fileData);
        });
    }

    // 設定
    let data;
    try {
        data = JSON.parse(GM_getValue("settings"));
    } catch (_) {
        data = {};
    }
    let settingKeys = [
        "theme",
        "cursorStyle",
        "tabSize",
        "useSoftTabs",
        "useWrapMode",
        "highlightActiveLine",
        "displayIndentGuides",
        "fontSize",
        "minLines",
        "maxLines",
    ];
    let defaultSettings = {
        theme: "tomorrow",
        cursorStyle: "ace",
        tabSize: 2,
        useSoftTabs: true,
        useWrapMode: false,
        highlightActiveLine: false,
        displayIndentGuides: true,
        fontSize: 12,
        minLines: 24,
        maxLines: 24,
    };
    let settingTypes = {
        theme: {"bright": ["chrome", "clouds", "crimson_editor", "dawn", "dreamweaver", "eclipse", "github", "iplastic", "solarized_light", "textmate", "tomorrow", "xcode", "kuroir", "katzenmilch", "sqlserver"], "dark": ["ambiance", "chaos", "clouds_midnight", "dracula", "cobalt", "gruvbox", "gob", "idle_fingers", "kr_theme", "merbivore", "merbivore_soft", "mono_industrial", "monokai", "nord_dark", "one_dark", "pastel_on_dark", "solarized_dark", "terminal", "tomorrow_night", "tomorrow_night_blue", "tomorrow_night_bright", "tomorrow_night_eighties", "twilight", "vibrant_ink"]},
        cursorStyle: ["ace", "slim", "smooth", "wide"],
        tabSize: "number",
        useSoftTabs: "checkbox",
        useWrapMode: "checkbox",
        highlightActiveLine: "checkbox",
        displayIndentGuides: "checkbox",
        fontSize: "number",
        minLines: "number",
        maxLines: "number",
    };
    for (let i of settingKeys) if (data[i] == undefined)  data[i] = defaultSettings[i];
    settingsButton.addEventListener("click", function() {
        const win = window.open("about:blank");
        const doc = win.document;
        doc.open();
        doc.write(`<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">`);
        doc.close();
        let settingDiv = doc.createElement("div");
        settingDiv.className = "panel panel-default";
        settingDiv.innerHTML = `
        <div class="panel-heading">Settings</div>
        <div class="panel-body">
            <form class="form-horizontal"></form>
        </div>
        `
        doc.body.prepend(settingDiv);
        let form = doc.getElementsByClassName("form-horizontal")[0];
        let reflectWhenChange = function(element) {
            element.addEventListener("change", function() {
                for (let i of settingKeys) {
                    if (settingTypes[i] == "number") data[i] = parseInt(doc.getElementById(i).value);
                    if (settingTypes[i] == "checkbox") data[i] = doc.getElementById(i).checked;
                    else data[i] = doc.getElementById(i).value;
                }
                GM_setValue("settings", JSON.stringify(data));
                colorize(newEditor);
            });
        };
        for (let i of settingKeys) {
            let div = doc.createElement("div");
            form.appendChild(div);
            div.className = "form-group";
            let label = doc.createElement("label");
            div.appendChild(label);
            label.className = "col-sm-3";
            label.for = i;
            label.innerText = i;
            if (Array.isArray(settingTypes[i])) {
                let select = doc.createElement("select");
                div.appendChild(select);
                select.id = i;
                for (let value of settingTypes[i]) {
                    let option = doc.createElement("option");
                    select.appendChild(option);
                    option.value = value.toLocaleLowerCase().replace(" ", "_");
                    option.innerText = value;
                    if (option.value == data[i]) option.selected = "true";
                }
                reflectWhenChange(select);
                continue;
            }
            if (typeof settingTypes[i] == "object") {
                let select = doc.createElement("select");
                div.appendChild(select);
                select.id = i;
                for (let key of Object.keys(settingTypes[i])) {
                    let optGroup = doc.createElement("optgroup");
                    select.appendChild(optGroup);
                    optGroup.label = key;
                    for (let value of settingTypes[i][key]) {
                        let option = doc.createElement("option");
                        optGroup.appendChild(option);
                        option.value = value;
                        option.innerText = value;
                        if (value == data[i]) option.selected = "true";
                    }
                }
                reflectWhenChange(select);
                continue;
            }
            let input = doc.createElement("input");
            div.appendChild(input);
            input.id = i;
            if (settingTypes[i] == "number") {
                input.type = "number";
                input.value = data[i].toString();
            } else if (settingTypes[i] == "checkbox") {
                input.type = "checkbox";
                input.checked = data[i];
            } else {
                console.error("Settings Option Error");
            }
            reflectWhenChange(input);
        }
        let resetButton = doc.createElement("button");
        doc.getElementsByClassName("panel-body")[0].appendChild(resetButton);
        resetButton.className = "btn btn-danger";
        resetButton.innerText = "Reset";
        resetButton.addEventListener("click", function() {
            if (!win.confirm("Are you sure you want to reset settings?")) return;
            for (let i of settingKeys) {
                data[i] = defaultSettings[i];
                let input = doc.getElementById(i);
                if (settingTypes[i] == "number") input.value = data[i].toString();
                else if (settingTypes[i] == "checkbox") input.checked = data[i];
                else input.value = data[i];
            }
        });
    });


    // 折りたたみ
    if (isReadOnly) {
        document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0].addEventListener("click", function() {
            if (this.innerText == this.dataset.onText) {
                newEditor.setOptions({
                    maxLines: data.maxLines,
                });
            } else {
                newEditor.setOptions({
                    maxLines: Infinity,
                });
            }
        });
    } else {
        document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].addEventListener("click", function() {
            if (this.classList.contains("active")) {
                newEditor.setOptions({
                    maxLines: data.maxLines,
                });
            } else {
                newEditor.setOptions({
                    maxLines: Infinity,
                });
            }
        });
    }

    // エディタの色付け
    let colorize = function(editor) {
        let lang = isReadOnly ? document.getElementsByClassName("text-center")[3].innerText : languageButton.innerText;
        lang = lang.slice(0, lang.indexOf(" ")).toLocaleLowerCase().replace("#", "sharp");
        if (lang.startsWith("pypy") || lang == "cython") lang = "python";
        else if (lang == "c++" || lang == "c") lang = "c_cpp";
        else if (lang.startsWith("cobol")) lang = "cobol";
        editor.session.setMode("ace/mode/" + lang);
        editor.session.setUseWrapMode(data.useWrapMode);
        editor.setTheme("ace/theme/" + data.theme);
        for (let key of settingKeys) {
            if (key == "theme" || key == "useWrapMode") continue;
            if (isReadOnly && key == "minLines") continue;
            editor.setOption(key, data[key]);
        }
        editor.setOption("fontSize", data.fontSize.toString() + "px");
        if (isReadOnly) {
            let expandButton = document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0];
            editor.setOption("readOnly", true);
            if (expandButton.innerText == expandButton.dataset.onText) {
                newEditor.setOptions({
                    maxLines: data.maxLines,
                });
            } else {
                newEditor.setOptions({
                    maxLines: Infinity,
                });
            }
        } else {
            if (document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].classList.contains("active")) {
                newEditor.setOptions({
                    minLines: data.minLines,
                    maxLines: Infinity,
                });
            } else {
                newEditor.setOptions({
                    minLines: data.minLines,
                    maxLines: data.maxLines,
                });
            }
        }
    };

    // ソースコードバイト数表示
    let sourceCodeLabel;
    let sourceCodeText;
    for (let element of document.getElementsByClassName("control-label col-sm-2")) {
        if (element.htmlFor == "sourceCode") {
            sourceCodeLabel = element;
            sourceCodeText = sourceCodeLabel.innerText;
            sourceCodeLabel.innerHTML += `<br>${(new Blob([originalEditor.getValue()])).size} Byte`;
            break;
        }
    }

    // Ace Editorがロードされたらエディタ作成
    requireJS.addEventListener("load", function() {
        aceEditor.addEventListener("load", function() {
            require.config({ paths: { "1.5.1": "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1" } });

            require(["1.5.1/ace"], function() {
                newEditor = ace.edit("new-div");
                newEditor.setValue(isReadOnly ? document.getElementById("for_copy0").innerText : originalEditor.getValue(), 1);
                colorize(newEditor);

                // languageButtonを監視
                if (!isReadOnly) {
                    let observer = new MutationObserver(function() {
                        colorize(newEditor);
                    });
                    const config = {
                        attributes: true,
                        childList: true,
                        characterData: true,
                    };
                    observer.observe(languageButton, config);
                }

                // ソースコードバイト数の変更
                newEditor.session.addEventListener("change", function() {
                    sourceCodeLabel.innerHTML = sourceCodeText + `<br>${(new Blob([newEditor.getValue()])).size} Byte`;
                });
            });
        });
    });
})();