AtCoder Comfortable Editor

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

As of 29.04.2023. See ბოლო ვერსია.

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