AtCoder Comfortable Editor

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

Versione datata 26/05/2022. Vedi la nuova versione l'ultima versione.

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