AtCoder Comfortable Editor

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

As of 2023-04-15. See the latest version.

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