- // ==UserScript==
- // @name AtCoder Comfortable Editor
- // @namespace http://tampermonkey.net/
- // @version 2.1
- // @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/*
- // @match https://atcoder.jp/contests/*/editorial*
- // @require https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js
- // @grant GM_getValue
- // @grant GM_setValue
- // ==/UserScript==
-
- (() => {
-
- let callCnt = 0;
-
- window.addEventListener("load", () => {
- if (location.pathname.endsWith("editorial") || location.pathname.endsWith("editorial/")) {
- for (let i of document.getElementsByClassName("label label-default")) {
- let observer = new MutationObserver(() => main(i.parentElement));
- const config = {
- childList: true,
- characterData: true,
- };
- observer.observe(i.parentElement, config);
- }
- } else main(document);
- });
-
- let main = subject => {
- ++callCnt;
- "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 isCustomTest = location.pathname.indexOf("custom_test") != -1;
- let isEditorial = location.pathname.indexOf("editorial") != -1;
- let isReadOnly = isEditorial || location.pathname.indexOf("submissions") != -1;
- let originalDiv;
- let originalDivs;
- if (isReadOnly) {
- originalDivs = [];
- for (let i of subject.getElementsByClassName("prettyprint linenums prettyprinted")) {
- originalDivs.push(i);
- }
- if (originalDivs.length == 0) return;
- } else {
- originalDiv = document.getElementsByClassName("div-editor")[0];
- if (originalDiv == undefined) return;
- }
-
- // 見た目変更
- if (isReadOnly) {
- for (let i of subject.getElementsByClassName("btn-copy btn-pre")) {
- i.style.zIndex = "7";
- i.style.borderRadius = "0";
- }
- } else {
- document.getElementsByClassName("form-control plain-textarea")[0].style.display = "none";
- 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 newDiv;
- let newDivs;
- let originalTexts;
- let originalEditor;
- let newEditor;
- let newEditors;
- let syncEditor;
- if (isEditorial) {
- newDivs = [];
- originalTexts = [];
- originalDivs.forEach((elm, i) => {
- originalTexts.push(elm.innerText);
- elm.style.display = "none";
- let newDiv = document.createElement("div");
- newDiv.id = "new-div-" + String(callCnt) + "-" + String(i);
- newDiv.style.marginTop = "10px";
- elm.after(newDiv);
- newDivs.push(newDiv);
- });
- } else {
- newDiv = document.createElement("div");
- newDiv.id = "new-div";
- newDiv.style.marginTop = "10px";
- newDiv.style.marginBottom = "10px";
- if (isReadOnly) {
- originalDivs[0].after(newDiv);
- originalDivs[0].style.display = "none";
- } else {
- originalEditor = $(".editor").data("editor").doc;
- originalDiv.after(newDiv);
- originalDiv.style.display = "none";
- syncEditor = () => {
- code = newEditor.getValue();
- originalEditor.setValue(newEditor.getValue());
- };
- }
- }
-
- // ボタン
- let languageButton;
- let languageButtons;
- let settingsButton;
- languageButton = document.querySelector("[id^=select2-dataLanguageId]");
- if (isEditorial) {
- languageButtons = [];
- newDivs.forEach((elm, i) => {
- let label = document.createElement("label");
- elm.after(label);
- label.innerText = "Python";
- label.for = "checkbox-" + String(callCnt) + "-" + String(i);
- let checkbox = document.createElement("input");
- label.prepend(checkbox);
- checkbox.type = "checkbox";
- checkbox.name = "checkbox-" + String(callCnt) + "-" + String(i);
- checkbox.id = "checkbox-" + String(callCnt) + "-" + String(i);
- languageButtons.push(checkbox);
- });
- } else {
- 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 (!isReadOnly) {
- code = originalEditor.getValue();
- // ページを去るときに警告
- 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];
- if (!isEditorial) {
- 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, button = null) => {
- let lang;
- if (isEditorial) {
- if (button.checked) lang = "python";
- else lang = "c_cpp";
- } else {
- 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);
- if (isEditorial) {
- editor.setOptions({
- maxLines: Infinity,
- });
- } else {
- let expandButton = document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0];
- if (expandButton.innerText == expandButton.dataset.onText) {
- editor.setOptions({
- maxLines: data.maxLines,
- });
- } else {
- editor.setOptions({
- maxLines: Infinity,
- });
- }
- }
- } else {
- if (document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].classList.contains("active")) {
- editor.setOptions({
- minLines: data.minLines,
- maxLines: Infinity,
- });
- } else {
- editor.setOptions({
- minLines: data.minLines,
- maxLines: data.maxLines,
- });
- }
- }
- };
-
- // ソースコードバイト数表示
- let sourceCodeLabel;
- let sourceCodeText;
- if (!isReadOnly) {
- 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"], () => {
- if (isEditorial) {
- newEditors = [];
- newDivs.forEach((elm, i) => {
- let newEditor = ace.edit(elm.id);
- newEditor.setValue(originalTexts[i], 1);
- colorize(newEditor, languageButtons[i]);
- newEditors.push(newEditor);
- });
- } else {
- 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);
- let observer2 = new MutationObserver(() => {
- colorize(newEditor);
- console.log("Yey");
- });
- const config2 = {
- attributes: true,
- childList: true,
- characterData: true,
- };
- observer2.observe(document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0], config2);
- } else if (isEditorial) {
- newEditors.forEach((editor, i) => {
- languageButtons[i].addEventListener("click", () => colorize(editor, languageButtons[i]));
- });
- } else {
- let observer = new MutationObserver(() => {
- colorize(newEditor);
- });
- const config = {
- childList: true,
- characterData: true,
- };
- observer.observe(document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0], config);
- }
-
- // ソースコードバイト数の変更
- if (!isReadOnly) {
- newEditor.session.addEventListener("change", () => {
- sourceCodeLabel.innerHTML = sourceCodeText + `<br>${(new Blob([newEditor.getValue()])).size} Byte`;
- });
- }
- });
- };
- aceEditor.addEventListener("load", prepare);
- document.head.prepend(aceEditor);
- };
-
- })();