AtCoder Comfortable Editor

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

目前為 2022-05-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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`;
                });
            });
        });
    });
})();