Greasy Fork is available in English.

AtCoder Comfortable Editor

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

Tính đến 27-05-2022. Xem phiên bản mới nhất.

  1. // ==UserScript==
  2. // @name AtCoder Comfortable Editor
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.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. // @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;
  148. try {
  149. data = JSON.parse(GM_getValue("settings"));
  150. } catch (_) {
  151. data = {};
  152. }
  153. let settingKeys = [
  154. "theme",
  155. "cursorStyle",
  156. "tabSize",
  157. "useSoftTabs",
  158. "useWrapMode",
  159. "highlightActiveLine",
  160. "displayIndentGuides",
  161. "fontSize",
  162. "minLines",
  163. "maxLines",
  164. ];
  165. let defaultSettings = {
  166. theme: "tomorrow",
  167. cursorStyle: "ace",
  168. tabSize: 2,
  169. useSoftTabs: true,
  170. useWrapMode: false,
  171. highlightActiveLine: false,
  172. displayIndentGuides: true,
  173. fontSize: 12,
  174. minLines: 24,
  175. maxLines: 24,
  176. };
  177. let settingTypes = {
  178. 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"]},
  179. cursorStyle: ["ace", "slim", "smooth", "wide"],
  180. tabSize: "number",
  181. useSoftTabs: "checkbox",
  182. useWrapMode: "checkbox",
  183. highlightActiveLine: "checkbox",
  184. displayIndentGuides: "checkbox",
  185. fontSize: "number",
  186. minLines: "number",
  187. maxLines: "number",
  188. };
  189. for (let i of settingKeys) if (data[i] == undefined) data[i] = defaultSettings[i];
  190. settingsButton.addEventListener("click", function() {
  191. const win = window.open("about:blank");
  192. const doc = win.document;
  193. doc.open();
  194. doc.write(`<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">`);
  195. doc.close();
  196. let settingDiv = doc.createElement("div");
  197. settingDiv.className = "panel panel-default";
  198. settingDiv.innerHTML = `
  199. <div class="panel-heading">Settings</div>
  200. <div class="panel-body">
  201. <form class="form-horizontal"></form>
  202. </div>
  203. `
  204. doc.body.prepend(settingDiv);
  205. let form = doc.getElementsByClassName("form-horizontal")[0];
  206. let reflectWhenChange = function(element) {
  207. element.addEventListener("change", function() {
  208. for (let i of settingKeys) {
  209. if (settingTypes[i] == "number") data[i] = parseInt(doc.getElementById(i).value);
  210. if (settingTypes[i] == "checkbox") data[i] = doc.getElementById(i).checked;
  211. else data[i] = doc.getElementById(i).value;
  212. }
  213. GM_setValue("settings", JSON.stringify(data));
  214. colorize(newEditor);
  215. });
  216. };
  217. for (let i of settingKeys) {
  218. let div = doc.createElement("div");
  219. form.appendChild(div);
  220. div.className = "form-group";
  221. let label = doc.createElement("label");
  222. div.appendChild(label);
  223. label.className = "col-sm-3";
  224. label.for = i;
  225. label.innerText = i;
  226. if (Array.isArray(settingTypes[i])) {
  227. let select = doc.createElement("select");
  228. div.appendChild(select);
  229. select.id = i;
  230. for (let value of settingTypes[i]) {
  231. let option = doc.createElement("option");
  232. select.appendChild(option);
  233. option.value = value.toLocaleLowerCase().replace(" ", "_");
  234. option.innerText = value;
  235. if (option.value == data[i]) option.selected = "true";
  236. }
  237. reflectWhenChange(select);
  238. continue;
  239. }
  240. if (typeof settingTypes[i] == "object") {
  241. let select = doc.createElement("select");
  242. div.appendChild(select);
  243. select.id = i;
  244. for (let key of Object.keys(settingTypes[i])) {
  245. let optGroup = doc.createElement("optgroup");
  246. select.appendChild(optGroup);
  247. optGroup.label = key;
  248. for (let value of settingTypes[i][key]) {
  249. let option = doc.createElement("option");
  250. optGroup.appendChild(option);
  251. option.value = value;
  252. option.innerText = value;
  253. if (value == data[i]) option.selected = "true";
  254. }
  255. }
  256. reflectWhenChange(select);
  257. continue;
  258. }
  259. let input = doc.createElement("input");
  260. div.appendChild(input);
  261. input.id = i;
  262. if (settingTypes[i] == "number") {
  263. input.type = "number";
  264. input.value = data[i].toString();
  265. } else if (settingTypes[i] == "checkbox") {
  266. input.type = "checkbox";
  267. input.checked = data[i];
  268. } else {
  269. console.error("Settings Option Error");
  270. }
  271. reflectWhenChange(input);
  272. }
  273. let resetButton = doc.createElement("button");
  274. doc.getElementsByClassName("panel-body")[0].appendChild(resetButton);
  275. resetButton.className = "btn btn-danger";
  276. resetButton.innerText = "Reset";
  277. resetButton.addEventListener("click", function() {
  278. if (!win.confirm("Are you sure you want to reset settings?")) return;
  279. for (let i of settingKeys) {
  280. data[i] = defaultSettings[i];
  281. let input = doc.getElementById(i);
  282. if (settingTypes[i] == "number") input.value = data[i].toString();
  283. else if (settingTypes[i] == "checkbox") input.checked = data[i];
  284. else input.value = data[i];
  285. }
  286. });
  287. });
  288.  
  289.  
  290. // 折りたたみ
  291. if (isReadOnly) {
  292. document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0].addEventListener("click", function() {
  293. if (this.innerText == this.dataset.onText) {
  294. newEditor.setOptions({
  295. maxLines: data.maxLines,
  296. });
  297. } else {
  298. newEditor.setOptions({
  299. maxLines: Infinity,
  300. });
  301. }
  302. });
  303. } else {
  304. document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].addEventListener("click", function() {
  305. if (this.classList.contains("active")) {
  306. newEditor.setOptions({
  307. maxLines: data.maxLines,
  308. });
  309. } else {
  310. newEditor.setOptions({
  311. maxLines: Infinity,
  312. });
  313. }
  314. });
  315. }
  316.  
  317. // エディタの色付け
  318. let colorize = function(editor) {
  319. let lang = isReadOnly ? document.getElementsByClassName("text-center")[3].innerText : languageButton.innerText;
  320. lang = lang.slice(0, lang.indexOf(" ")).toLocaleLowerCase().replace("#", "sharp");
  321. if (lang.startsWith("pypy") || lang == "cython") lang = "python";
  322. else if (lang == "c++" || lang == "c") lang = "c_cpp";
  323. else if (lang.startsWith("cobol")) lang = "cobol";
  324. editor.session.setMode("ace/mode/" + lang);
  325. editor.session.setUseWrapMode(data.useWrapMode);
  326. editor.setTheme("ace/theme/" + data.theme);
  327. for (let key of settingKeys) {
  328. if (key == "theme" || key == "useWrapMode") continue;
  329. if (isReadOnly && key == "minLines") continue;
  330. editor.setOption(key, data[key]);
  331. }
  332. editor.setOption("fontSize", data.fontSize.toString() + "px");
  333. if (isReadOnly) {
  334. let expandButton = document.getElementsByClassName("btn-text toggle-btn-text source-code-expand-btn")[0];
  335. editor.setOption("readOnly", true);
  336. if (expandButton.innerText == expandButton.dataset.onText) {
  337. newEditor.setOptions({
  338. maxLines: data.maxLines,
  339. });
  340. } else {
  341. newEditor.setOptions({
  342. maxLines: Infinity,
  343. });
  344. }
  345. } else {
  346. if (document.getElementsByClassName("btn btn-default btn-sm btn-auto-height")[0].classList.contains("active")) {
  347. newEditor.setOptions({
  348. minLines: data.minLines,
  349. maxLines: Infinity,
  350. });
  351. } else {
  352. newEditor.setOptions({
  353. minLines: data.minLines,
  354. maxLines: data.maxLines,
  355. });
  356. }
  357. }
  358. };
  359.  
  360. // ソースコードバイト数表示
  361. let sourceCodeLabel;
  362. let sourceCodeText;
  363. for (let element of document.getElementsByClassName("control-label col-sm-2")) {
  364. if (element.htmlFor == "sourceCode") {
  365. sourceCodeLabel = element;
  366. sourceCodeText = sourceCodeLabel.innerText;
  367. sourceCodeLabel.innerHTML += `<br>${(new Blob([originalEditor.getValue()])).size} Byte`;
  368. break;
  369. }
  370. }
  371.  
  372. // Ace Editorがロードされたらエディタ作成
  373. requireJS.addEventListener("load", function() {
  374. aceEditor.addEventListener("load", function() {
  375. require.config({ paths: { "1.5.1": "https://cdnjs.cloudflare.com/ajax/libs/ace/1.5.1" } });
  376.  
  377. require(["1.5.1/ace"], function() {
  378. newEditor = ace.edit("new-div");
  379. newEditor.setValue(isReadOnly ? document.getElementById("for_copy0").innerText : originalEditor.getValue(), 1);
  380. colorize(newEditor);
  381.  
  382. // languageButtonを監視
  383. if (!isReadOnly) {
  384. let observer = new MutationObserver(function() {
  385. colorize(newEditor);
  386. });
  387. const config = {
  388. attributes: true,
  389. childList: true,
  390. characterData: true,
  391. };
  392. observer.observe(languageButton, config);
  393. }
  394.  
  395. // ソースコードバイト数の変更
  396. newEditor.session.addEventListener("change", function() {
  397. sourceCodeLabel.innerHTML = sourceCodeText + `<br>${(new Blob([newEditor.getValue()])).size} Byte`;
  398. });
  399. });
  400. });
  401. });
  402. })();