AtCoder Comfortable Editor

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

2022/12/25のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name AtCoder Comfortable Editor
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4
  5. // @description AtCoderのコードテスト・提出欄・提出コードを快適にします
  6. // @author Chippppp
  7. // @license MIT
  8. // @match https://atcoder.jp/contests/*/custom_test*
  9. // @match https://atcoder.jp/contests/*/submit*
  10. // @match https://atcoder.jp/contests/*/tasks/*
  11. // @match https://atcoder.jp/contests/*/submissions/*
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // ==/UserScript==
  16.  
  17. (() => {
  18. "use strict";
  19.  
  20. // Ace Editor in cdnjs
  21. // Copyright (c) 2010, Ajax.org B.V.
  22. let aceEditor = document.createElement("script");
  23. aceEditor.src = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1/ace.js";
  24.  
  25. let isReadOnly = location.pathname.indexOf("submissions") != -1;
  26. let isCustomTest = location.pathname.indexOf("custom_test") != -1;
  27. if (isReadOnly && document.getElementById("submission-code") == undefined) return;
  28. if (!isReadOnly && document.getElementsByClassName("div-editor")[0] == undefined) return;
  29.  
  30. // 見た目変更
  31. if (isReadOnly) {
  32. document.getElementsByClassName("linenums")[0].style.display = "none";
  33. document.getElementsByClassName("btn-copy btn-pre")[0].style.zIndex = "7";
  34. document.getElementsByClassName("btn-copy btn-pre")[1].style.zIndex = "7";
  35. document.getElementsByClassName("btn-copy btn-pre")[0].style.borderRadius = "0";
  36. document.getElementsByClassName("btn-copy btn-pre")[1].style.borderRadius = "0";
  37. } else {
  38. document.getElementsByClassName("btn btn-default btn-sm btn-toggle-editor")[0].style.display = "none";
  39. document.getElementsByClassName("btn btn-default btn-sm btn-toggle-editor")[0].classList.remove("active");
  40. }
  41.  
  42. // エディタ
  43. let originalDiv;
  44. let newDiv = document.createElement("div");
  45. newDiv.id = "new-div";
  46. newDiv.style.marginTop = "10px";
  47. newDiv.style.marginBottom = "10px";
  48. let originalEditor;
  49. let newEditor;
  50. let syncEditor;
  51. if (isReadOnly) {
  52. document.getElementById("submission-code").after(newDiv);
  53. } else {
  54. originalDiv = document.getElementsByClassName("div-editor")[0];
  55. originalDiv.style.display = "none";
  56. document.getElementsByClassName("form-control plain-textarea")[0].style.display = "none";
  57. originalEditor = $(".editor").data("editor").doc;
  58. originalDiv.after(newDiv);
  59. syncEditor = () => {
  60. code = newEditor.getValue();
  61. originalEditor.setValue(newEditor.getValue());
  62. };
  63. }
  64.  
  65. // ボタン
  66. let languageButton;
  67. let settingsButton;
  68. languageButton = document.querySelector("[id^=select2-dataLanguageId]");
  69. settingsButton = document.createElement("button");
  70. newDiv.after(settingsButton);
  71. settingsButton.className = "btn btn-secondary btn-sm";
  72. settingsButton.type = "button";
  73. settingsButton.innerText = "Editor Settings";
  74. if (!isReadOnly && !isCustomTest) {
  75. let copyP = document.createElement("p");
  76. document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].parentElement.after(copyP);
  77. let copyButton = document.createElement("button");
  78. copyP.appendChild(copyButton);
  79. copyButton.className = "btn btn-info btn-sm";
  80. copyButton.type = "button";
  81. copyButton.innerText = "Copy From Code Test";
  82. copyButton.addEventListener("click", () => {
  83. let href = location.href;
  84. if (href.indexOf("tasks") != -1) href = href.slice(0, href.indexOf("tasks"));
  85. else href = href.slice(0, href.indexOf("submit"));
  86. href += "custom_test";
  87. fetch(href).then(response => response.text()).then(data => {
  88. const parser = new DOMParser();
  89. const doc = parser.parseFromString(data, "text/html");
  90. newEditor.setValue(doc.getElementsByClassName("editor")[0].value, 1);
  91. });
  92. });
  93. }
  94.  
  95. // 保存されたコード
  96. let code;
  97. if (!isReadOnly) {
  98. code = originalEditor.getValue();
  99. // ページを去るときに警告
  100. window.addEventListener("beforeunload", e => {
  101. if (newEditor.getValue() != code) e.returnValue = "The code is not saved, are you sure you want to leave the page?";
  102. });
  103. }
  104.  
  105. // ボタンでエディターを同期
  106. if (!isReadOnly) {
  107. let buttons = Array.from(Array.from(document.getElementsByClassName("col-sm-5")).slice(-1)[0].children);
  108. for (let originalButton of buttons) {
  109. if (originalButton.tagName.toLowerCase() != "button" && !originalButton.classList.contains("btn")) continue;
  110. let newButton = originalButton.cloneNode(true);
  111. originalButton.after(newButton);
  112. originalButton.id = "";
  113. originalButton.style.display = "none";
  114. newButton.addEventListener("click", e => {
  115. e.preventDefault();
  116. syncEditor();
  117. originalButton.click();
  118. });
  119. }
  120. if (isCustomTest) {
  121. let submit = vueCustomTest.submit;
  122. vueCustomTest.submit = () => {
  123. syncEditor();
  124. submit();
  125. };
  126. }
  127. }
  128.  
  129. // 互換性のため
  130. if (!isReadOnly) getSourceCode = () => newEditor.getValue();
  131.  
  132. // ファイルを開く場合
  133. if (!isReadOnly) {
  134. document.getElementById("input-open-file").addEventListener("change", e => {
  135. let fileData = e.target.files[0];
  136. let reader = new FileReader();
  137. reader.addEventListener("load", () => {
  138. newEditor.setValue(reader.result);
  139. });
  140. reader.readAsText(fileData);
  141. });
  142. }
  143.  
  144. // 設定
  145. let data;
  146. try {
  147. data = JSON.parse(GM_getValue("settings"));
  148. } catch (_) {
  149. data = {};
  150. }
  151. let settingKeys = [
  152. "theme",
  153. "cursorStyle",
  154. "tabSize",
  155. "useSoftTabs",
  156. "useWrapMode",
  157. "highlightActiveLine",
  158. "displayIndentGuides",
  159. "fontSize",
  160. "minLines",
  161. "maxLines",
  162. ];
  163. let defaultSettings = {
  164. theme: "tomorrow",
  165. cursorStyle: "ace",
  166. tabSize: 2,
  167. useSoftTabs: true,
  168. useWrapMode: false,
  169. highlightActiveLine: false,
  170. displayIndentGuides: true,
  171. fontSize: 12,
  172. minLines: 24,
  173. maxLines: 24,
  174. };
  175. let settingTypes = {
  176. 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"]},
  177. cursorStyle: ["ace", "slim", "smooth", "wide"],
  178. tabSize: "number",
  179. useSoftTabs: "checkbox",
  180. useWrapMode: "checkbox",
  181. highlightActiveLine: "checkbox",
  182. displayIndentGuides: "checkbox",
  183. fontSize: "number",
  184. minLines: "number",
  185. maxLines: "number",
  186. };
  187. for (let i of settingKeys) if (data[i] == undefined) data[i] = defaultSettings[i];
  188. settingsButton.addEventListener("click", () => {
  189. const win = window.open("about:blank");
  190. const doc = win.document;
  191. doc.open();
  192. doc.write(`<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">`);
  193. doc.close();
  194. let settingDiv = doc.createElement("div");
  195. settingDiv.className = "panel panel-default";
  196. settingDiv.innerHTML = `
  197. <div class="panel-heading">Settings</div>
  198. <div class="panel-body">
  199. <form class="form-horizontal"></form>
  200. </div>
  201. `
  202. doc.body.prepend(settingDiv);
  203. let form = doc.getElementsByClassName("form-horizontal")[0];
  204. let reflectWhenChange = element => {
  205. element.addEventListener("change", () => {
  206. for (let i of settingKeys) {
  207. if (settingTypes[i] == "number") data[i] = parseInt(doc.getElementById(i).value);
  208. if (settingTypes[i] == "checkbox") data[i] = doc.getElementById(i).checked;
  209. else data[i] = doc.getElementById(i).value;
  210. }
  211. GM_setValue("settings", JSON.stringify(data));
  212. colorize(newEditor);
  213. });
  214. };
  215. for (let i of settingKeys) {
  216. let div = doc.createElement("div");
  217. form.appendChild(div);
  218. div.className = "form-group";
  219. let label = doc.createElement("label");
  220. div.appendChild(label);
  221. label.className = "col-sm-3";
  222. label.for = i;
  223. label.innerText = i;
  224. if (Array.isArray(settingTypes[i])) {
  225. let select = doc.createElement("select");
  226. div.appendChild(select);
  227. select.id = i;
  228. for (let value of settingTypes[i]) {
  229. let option = doc.createElement("option");
  230. select.appendChild(option);
  231. option.value = value.toLocaleLowerCase().replace(" ", "_");
  232. option.innerText = value;
  233. if (option.value == data[i]) option.selected = "true";
  234. }
  235. reflectWhenChange(select);
  236. continue;
  237. }
  238. if (typeof settingTypes[i] == "object") {
  239. let select = doc.createElement("select");
  240. div.appendChild(select);
  241. select.id = i;
  242. for (let key of Object.keys(settingTypes[i])) {
  243. let optGroup = doc.createElement("optgroup");
  244. select.appendChild(optGroup);
  245. optGroup.label = key;
  246. for (let value of settingTypes[i][key]) {
  247. let option = doc.createElement("option");
  248. optGroup.appendChild(option);
  249. option.value = value;
  250. option.innerText = value;
  251. if (value == data[i]) option.selected = "true";
  252. }
  253. }
  254. reflectWhenChange(select);
  255. continue;
  256. }
  257. let input = doc.createElement("input");
  258. div.appendChild(input);
  259. input.id = i;
  260. if (settingTypes[i] == "number") {
  261. input.type = "number";
  262. input.value = data[i].toString();
  263. } else if (settingTypes[i] == "checkbox") {
  264. input.type = "checkbox";
  265. input.checked = data[i];
  266. } else {
  267. console.error("Settings Option Error");
  268. }
  269. reflectWhenChange(input);
  270. }
  271. let resetButton = doc.createElement("button");
  272. doc.getElementsByClassName("panel-body")[0].appendChild(resetButton);
  273. resetButton.className = "btn btn-danger";
  274. resetButton.innerText = "Reset";
  275. resetButton.addEventListener("click", () => {
  276. if (!win.confirm("Are you sure you want to reset settings?")) return;
  277. for (let i of settingKeys) {
  278. data[i] = defaultSettings[i];
  279. let input = doc.getElementById(i);
  280. if (settingTypes[i] == "number") input.value = data[i].toString();
  281. else if (settingTypes[i] == "checkbox") input.checked = data[i];
  282. else input.value = data[i];
  283. }
  284. });
  285. });
  286.  
  287. // エディタの色付け
  288. let colorize = editor => {
  289. let lang = isReadOnly ? document.getElementsByClassName("text-center")[3].innerText : languageButton.innerText;
  290. lang = lang.slice(0, lang.indexOf(" ")).toLowerCase().replace("#", "sharp").replace(/[0-9]/g, "");
  291. if (lang.startsWith("pypy") || lang == "cython") lang = "python";
  292. else if (lang == "c++" || lang == "c") lang = "c_cpp";
  293. else if (lang.startsWith("cobol")) lang = "cobol";
  294. editor.session.setMode("ace/mode/" + lang);
  295. editor.session.setUseWrapMode(data.useWrapMode);
  296. editor.setTheme("ace/theme/" + data.theme);
  297. for (let key of settingKeys) {
  298. if (key == "theme" || key == "useWrapMode") continue;
  299. if (isReadOnly && key == "minLines") continue;
  300. editor.setOption(key, data[key]);
  301. }
  302. editor.setOption("fontSize", data.fontSize.toString() + "px");
  303. if (isReadOnly) {
  304. editor.setOption("readOnly", true);
  305. let expandButton = document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0];
  306. if (expandButton.innerText == expandButton.dataset.onText) {
  307. newEditor.setOptions({
  308. maxLines: data.maxLines,
  309. });
  310. } else {
  311. newEditor.setOptions({
  312. maxLines: Infinity,
  313. });
  314. }
  315. } else {
  316. if (document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].classList.contains("active")) {
  317. newEditor.setOptions({
  318. minLines: data.minLines,
  319. maxLines: Infinity,
  320. });
  321. } else {
  322. newEditor.setOptions({
  323. minLines: data.minLines,
  324. maxLines: data.maxLines,
  325. });
  326. }
  327. }
  328. };
  329.  
  330. // ソースコードバイト数表示
  331. let sourceCodeLabel;
  332. let sourceCodeText;
  333. for (let element of document.getElementsByClassName("control-label col-sm-2")) {
  334. if (element.htmlFor == "sourceCode") {
  335. sourceCodeLabel = element;
  336. sourceCodeText = sourceCodeLabel.innerText;
  337. sourceCodeLabel.innerHTML += `<br>${(new Blob([originalEditor.getValue()])).size} Byte`;
  338. break;
  339. }
  340. }
  341.  
  342. // ロードされたらエディタ作成
  343. let prepare = () => {
  344. require.config({ paths: { "1.5.1": "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1" } });
  345.  
  346. require(["1.5.1/ace"], () => {
  347. newEditor = ace.edit("new-div");
  348. newEditor.setValue(isReadOnly ? document.getElementById("for_copy0").innerText : originalEditor.getValue(), 1);
  349. colorize(newEditor);
  350.  
  351. // languageButtonを監視
  352. if (!isReadOnly) {
  353. let observer = new MutationObserver(() => {
  354. colorize(newEditor);
  355. });
  356. const config = {
  357. attributes: true,
  358. childList: true,
  359. characterData: true,
  360. };
  361. observer.observe(languageButton, config);
  362. }
  363.  
  364. // ソースコードバイト数の変更
  365. newEditor.session.addEventListener("change", () => {
  366. sourceCodeLabel.innerHTML = sourceCodeText + `<br>${(new Blob([newEditor.getValue()])).size} Byte`;
  367. });
  368. });
  369. };
  370. aceEditor.addEventListener("load", prepare);
  371. document.head.prepend(aceEditor);
  372. })();