AtCoder Comfortable Editor

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

Від 16.06.2022. Дивіться остання версія.

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

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

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

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

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

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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

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

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

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

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

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

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

// ==UserScript==
// @name    AtCoder 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();
    });
})();