您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoderのコードテスト・提出欄・提出コードを快適にします
当前为
// ==UserScript== // @name AtCoder Comfortable Editor // @namespace http://tampermonkey.net/ // @version 1.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/* // @require https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (() => { "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"; 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 = () => { code = newEditor.getValue(); originalEditor.setValue(newEditor.getValue()); }; } // ボタン let languageButton; let settingsButton; languageButton = document.querySelector("[id^=select2-dataLanguageId]"); 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", () => { 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(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", 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.tagName.toLowerCase() != "button" && !originalButton.classList.contains("btn")) continue; let newButton = originalButton.cloneNode(true); originalButton.after(newButton); originalButton.id = ""; originalButton.style.display = "none"; newButton.addEventListener("click", e => { e.preventDefault(); syncEditor(); originalButton.click(); }); } if (isCustomTest) { let submit = vueCustomTest.submit; vueCustomTest.submit = () => { syncEditor(); submit(); }; } } // 互換性のため if (!isReadOnly) getSourceCode = () => newEditor.getValue(); // ファイルを開く場合 if (!isReadOnly) { document.getElementById("input-open-file").addEventListener("change", e => { let fileData = e.target.files[0]; let reader = new FileReader(); reader.addEventListener("load", () => { 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", () => { 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 = element => { element.addEventListener("change", () => { 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", () => { 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 = editor => { let lang = isReadOnly ? document.getElementsByClassName("text-center")[3].innerText : languageButton.innerText; lang = lang.slice(0, lang.indexOf(" ")).toLowerCase().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 = () => { require.config({ paths: { "1.5.1": "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1" } }); require(["1.5.1/ace"], () => { 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(() => { colorize(newEditor); }); const config = { attributes: true, childList: true, characterData: true, }; observer.observe(languageButton, config); } // ソースコードバイト数の変更 newEditor.session.addEventListener("change", () => { sourceCodeLabel.innerHTML = sourceCodeText + `<br>${(new Blob([newEditor.getValue()])).size} Byte`; }); }); }; aceEditor.addEventListener("load", prepare); document.head.prepend(aceEditor); })();