Greasy Fork is available in English.

AtCoder Comfortable Editor

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

Version au 12/06/2022. Voir la dernière version.

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