// ==UserScript==
// @name AtCoder Comfortable Editor
// @namespace http://tampermonkey.net/
// @version 2.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/*
// @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;
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 {
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 (!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 {
if (languageButton == null) languageButton = document.querySelector("[id^=select2-dataLanguageId]");
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);
});
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);
};
})();