AtCoder Comfortable Editor

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

Ekde 2022/06/12. Vidu La ĝisdata versio.

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