AtCoder Comfortable Editor

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

À partir de 2022-06-16. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name    AtCoder Comfortable Editor
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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/*
// @require    https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js
// @grant    GM_getValue
// @grant    GM_setValue
// ==/UserScript==

(function() {
    "use strict";

    // 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 = location.pathname.indexOf("submissions") != -1;
    let isCustomTest = location.pathname.indexOf("custom_test") != -1;
    if (isReadOnly && document.getElementById("submission-code") == undefined) return;
    if (!isReadOnly && document.getElementsByClassName("div-editor")[0] == undefined) return;

    // 見た目変更
    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) {
            if (originalButton.tag != "button") continue;
            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];
            }
        });
    });

    // エディタの色付け
    let colorize = function(editor) {
        let lang = isReadOnly ? document.getElementsByClassName("text-center")[3].innerText : languageButton.innerText;
        lang = lang.slice(0, lang.indexOf(" ")).toLocaleLowerCase().replace("#", "sharp").replace(/[0-9]/g, "");
        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) {
            editor.setOption("readOnly", true);
            let expandButton = document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0];
            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;
        }
    }

    // ロードされたらエディタ作成
    let prepare = 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`;
            });
        });
    };
    aceEditor.addEventListener("load", function() {
        prepare();
    });
})();