AtCoder Comfortable Editor

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

目前为 2022-05-28 提交的版本。查看 最新版本

// ==UserScript==
// @name    AtCoder Comfortable Editor
// @namespace    http://tampermonkey.net/
// @version      0.3
// @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 = 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) {
            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`;
                });
            });
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址