AtCoder Easy Test v2

Make testing sample cases easy

Verze ze dne 29. 09. 2021. Zobrazit nejnovější verzi.

  1. // ==UserScript==
  2. // @name AtCoder Easy Test v2
  3. // @namespace https://atcoder.jp/
  4. // @version 2.0.1
  5. // @description Make testing sample cases easy
  6. // @author magurofly
  7. // @license MIT
  8. // @supportURL https://github.com/magurofly/atcoder-easy-test/
  9. // @match https://atcoder.jp/contests/*/tasks/*
  10. // @grant unsafeWindow
  11. // ==/UserScript==
  12. (function() {
  13. const codeSaver = {
  14. LIMIT: 10,
  15. get() {
  16. // `json` は、ソースコード文字列またはJSON文字列
  17. let json = unsafeWindow.localStorage.AtCoderEasyTest$lastCode;
  18. let data = [];
  19. try {
  20. if (typeof json == "string") {
  21. data.push(...JSON.parse(json));
  22. }
  23. else {
  24. data = [];
  25. }
  26. }
  27. catch (e) {
  28. data.push({
  29. path: unsafeWindow.localStorage.AtCoderEasyTset$lastPage,
  30. code: json,
  31. });
  32. }
  33. return data;
  34. },
  35. set(data) {
  36. unsafeWindow.localStorage.AtCoderEasyTest$lastCode = JSON.stringify(data);
  37. },
  38. save(code) {
  39. let data = codeSaver.get();
  40. const idx = data.findIndex(({ path }) => path == location.pathname);
  41. if (idx != -1)
  42. data.splice(idx, idx + 1);
  43. data.push({
  44. path: location.pathname,
  45. code,
  46. });
  47. while (data.length > codeSaver.LIMIT)
  48. data.shift();
  49. codeSaver.set(data);
  50. },
  51. restore() {
  52. const data = codeSaver.get();
  53. const idx = data.findIndex(({ path }) => path == location.pathname);
  54. if (idx == -1 || !(data[idx] instanceof Object))
  55. return Promise.reject(`No saved code found for ${location.pathname}`);
  56. return Promise.resolve(data[idx].code);
  57. }
  58. };
  59.  
  60. class CodeRunner {
  61. get label() {
  62. return this._label;
  63. }
  64. constructor(label, site) {
  65. this._label = `${label} [${site}]`;
  66. }
  67. async test(sourceCode, input, expectedOutput, options) {
  68. const result = await this.run(sourceCode, input);
  69. if (expectedOutput != null)
  70. result.expectedOutput = expectedOutput;
  71. if (result.status != "OK" || typeof expectedOutput != "string")
  72. return result;
  73. let output = result.output || "";
  74. if (options.trim) {
  75. expectedOutput = expectedOutput.trim();
  76. output = output.trim();
  77. }
  78. let equals = (x, y) => x === y;
  79. if (options.allowableError) {
  80. const floatPattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
  81. const superEquals = equals;
  82. equals = (x, y) => {
  83. if (floatPattern.test(x) && floatPattern.test(y))
  84. return Math.abs(parseFloat(x) - parseFloat(y)) <= options.allowableError;
  85. return superEquals(x, y);
  86. };
  87. }
  88. if (options.split) {
  89. const superEquals = equals;
  90. equals = (x, y) => {
  91. const xs = x.split(/\s+/);
  92. const ys = y.split(/\s+/);
  93. if (xs.length != ys.length)
  94. return false;
  95. const len = xs.length;
  96. for (let i = 0; i < len; i++) {
  97. if (!superEquals(xs[i], ys[i]))
  98. return false;
  99. }
  100. return true;
  101. };
  102. }
  103. result.status = equals(output, expectedOutput) ? "AC" : "WA";
  104. return result;
  105. }
  106. }
  107.  
  108. class CustomRunner extends CodeRunner {
  109. run;
  110. constructor(label, run) {
  111. super(label, "Browser");
  112. this.run = run;
  113. }
  114. }
  115.  
  116. function buildParams(data) {
  117. return Object.entries(data).map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  118. }
  119. function sleep(ms) {
  120. return new Promise(done => setTimeout(done, ms));
  121. }
  122.  
  123. let waitAtCoderCustomTest = Promise.resolve();
  124. const AtCoderCustomTestBase = location.href.replace(/\/tasks\/.+$/, "/custom_test");
  125. const AtCoderCustomTestResultAPI = AtCoderCustomTestBase + "/json?reload=true";
  126. const AtCoderCustomTestSubmitAPI = AtCoderCustomTestBase + "/submit/json";
  127. class AtCoderRunner extends CodeRunner {
  128. languageId;
  129. constructor(languageId, label) {
  130. super(label, "AtCoder");
  131. this.languageId = languageId;
  132. }
  133. async run(sourceCode, input) {
  134. const promise = this.submit(sourceCode, input);
  135. waitAtCoderCustomTest = promise;
  136. return await promise;
  137. }
  138. async submit(sourceCode, input) {
  139. try {
  140. await waitAtCoderCustomTest;
  141. }
  142. catch (error) {
  143. console.error(error);
  144. }
  145. const error = await fetch(AtCoderCustomTestSubmitAPI, {
  146. method: "POST",
  147. credentials: "include",
  148. headers: {
  149. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  150. },
  151. body: buildParams({
  152. "data.LanguageId": String(this.languageId),
  153. sourceCode,
  154. input,
  155. csrf_token: unsafeWindow.csrfToken,
  156. }),
  157. }).then(r => r.text());
  158. if (error) {
  159. throw new Error(error);
  160. }
  161. await sleep(100);
  162. for (;;) {
  163. const data = await fetch(AtCoderCustomTestResultAPI, {
  164. method: "GET",
  165. credentials: "include",
  166. }).then(r => r.json());
  167. if (!("Result" in data))
  168. continue;
  169. const result = data.Result;
  170. if ("Interval" in data) {
  171. await sleep(data.Interval);
  172. continue;
  173. }
  174. return {
  175. status: (result.ExitCode == 0) ? "OK" : (result.TimeConsumption == -1) ? "CE" : "RE",
  176. exitCode: result.ExitCode,
  177. execTime: result.TimeConsumption,
  178. memory: result.MemoryConsumption,
  179. input,
  180. output: data.Stdout,
  181. error: data.Stderr,
  182. };
  183. }
  184. }
  185. }
  186.  
  187. class PaizaIORunner extends CodeRunner {
  188. name;
  189. constructor(name, label) {
  190. super(label, "PaizaIO");
  191. this.name = name;
  192. }
  193. async run(sourceCode, input) {
  194. let id, status, error;
  195. try {
  196. const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
  197. source_code: sourceCode,
  198. language: this.name,
  199. input,
  200. longpoll: "true",
  201. longpoll_timeout: "10",
  202. api_key: "guest",
  203. }), {
  204. method: "POST",
  205. mode: "cors",
  206. }).then(r => r.json());
  207. id = res.id;
  208. status = res.status;
  209. error = res.error;
  210. }
  211. catch (error) {
  212. return {
  213. status: "IE",
  214. input,
  215. error: String(error),
  216. };
  217. }
  218. while (status == "running") {
  219. const res = await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
  220. id,
  221. api_key: "guest",
  222. }), {
  223. mode: "cors",
  224. }).then(res => res.json());
  225. status = res.status;
  226. error = res.error;
  227. }
  228. const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
  229. id,
  230. api_key: "guest",
  231. }), {
  232. mode: "cors",
  233. }).then(r => r.json());
  234. const result = {
  235. status: "OK",
  236. exitCode: String(res.exit_code),
  237. execTime: +res.time * 1e3,
  238. memory: +res.memory * 1e-3,
  239. input,
  240. };
  241. if (res.build_result == "failure") {
  242. result.status = "CE";
  243. result.exitCode = res.build_exit_code;
  244. result.output = res.build_stdout;
  245. result.error = res.build_stderr;
  246. }
  247. else {
  248. result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
  249. result.exitCode = res.exit_code;
  250. result.output = res.stdout;
  251. result.error = res.stderr;
  252. }
  253. return result;
  254. }
  255. }
  256.  
  257. class WandboxRunner extends CodeRunner {
  258. name;
  259. options;
  260. constructor(name, label, options = {}) {
  261. super(label, "Wandbox");
  262. this.name = name;
  263. this.options = options;
  264. }
  265. getOptions(sourceCode, input) {
  266. if (typeof this.options == "function")
  267. return this.options(sourceCode, input);
  268. return this.options;
  269. }
  270. run(sourceCode, input) {
  271. const options = this.getOptions(sourceCode, input);
  272. return this.request(Object.assign({
  273. compiler: this.name,
  274. code: sourceCode,
  275. stdin: input,
  276. }, options));
  277. }
  278. async request(body) {
  279. const startTime = Date.now();
  280. let res;
  281. try {
  282. res = await fetch("https://wandbox.org/api/compile.json", {
  283. method: "POST",
  284. mode: "cors",
  285. headers: {
  286. "Content-Type": "application/json",
  287. },
  288. body: JSON.stringify(body),
  289. }).then(r => r.json());
  290. }
  291. catch (error) {
  292. console.error(error);
  293. return {
  294. status: "IE",
  295. input: body.stdin,
  296. error: String(error),
  297. };
  298. }
  299. const endTime = Date.now();
  300. const result = {
  301. status: "OK",
  302. exitCode: String(res.status),
  303. execTime: endTime - startTime,
  304. input: body.stdin,
  305. output: String(res.program_output || ""),
  306. error: String(res.program_error || ""),
  307. };
  308. // 正常終了以外の場合
  309. if (res.status != 0) {
  310. if (res.signal) {
  311. result.exitCode += ` (${res.signal})`;
  312. }
  313. result.output = String(res.compiler_output || "") + String(result.output || "");
  314. result.error = String(res.compiler_error || "") + String(result.error || "");
  315. if (res.compiler_output || res.compiler_error) {
  316. result.status = "CE";
  317. }
  318. else {
  319. result.status = "RE";
  320. }
  321. }
  322. return result;
  323. }
  324. }
  325.  
  326. class WandboxCppRunner extends WandboxRunner {
  327. async run(sourceCode, input) {
  328. // ACL を結合する
  329. const ACLBase = "https://cdn.jsdelivr.net/gh/atcoder/ac-library/";
  330. const files = new Map();
  331. const includeHeader = async (source) => {
  332. const pattern = /^#\s*include\s*[<"]atcoder\/([^>"]+)[>"]/gm;
  333. const loaded = [];
  334. let match;
  335. while (match = pattern.exec(source)) {
  336. const file = "atcoder/" + match[1];
  337. if (files.has(file))
  338. continue;
  339. files.set(file, null);
  340. loaded.push([file, fetch(ACLBase + file, { mode: "cors", cache: "force-cache", }).then(r => r.text())]);
  341. }
  342. const included = await Promise.all(loaded.map(async ([file, r]) => {
  343. const source = await r;
  344. files.set(file, source);
  345. return source;
  346. }));
  347. for (const source of included) {
  348. await includeHeader(source);
  349. }
  350. };
  351. await includeHeader(sourceCode);
  352. const codes = [];
  353. for (const [file, code] of files) {
  354. codes.push({ file, code, });
  355. }
  356. const options = this.getOptions(sourceCode, input);
  357. return await this.request(Object.assign({
  358. compiler: this.name,
  359. code: sourceCode,
  360. stdin: input,
  361. codes,
  362. "compiler-option-raw": "-I.",
  363. }, options));
  364. }
  365. }
  366.  
  367. let brythonRunnerLoaded = false;
  368. const brythonRunner = new CustomRunner("Brython", async (sourceCode, input) => {
  369. if (!brythonRunnerLoaded) {
  370. // BrythonRunner を読み込む
  371. await new Promise((resolve) => {
  372. const script = document.createElement("script");
  373. script.src = "https://cdn.jsdelivr.net/gh/pythonpad/brython-runner/lib/brython-runner.bundle.js";
  374. script.onload = () => {
  375. brythonRunnerLoaded = true;
  376. resolve(null);
  377. };
  378. document.head.appendChild(script);
  379. });
  380. }
  381. let stdout = "";
  382. let stderr = "";
  383. let stdinOffset = 0;
  384. const BrythonRunner = unsafeWindow.BrythonRunner;
  385. const runner = new BrythonRunner({
  386. stdout: { write(content) { stdout += content; }, flush() { } },
  387. stderr: { write(content) { stderr += content; }, flush() { } },
  388. stdin: { async readline() {
  389. let index = input.indexOf("\n", stdinOffset) + 1;
  390. if (index == 0)
  391. index = input.length;
  392. const text = input.slice(stdinOffset, index);
  393. stdinOffset = index;
  394. return text;
  395. } },
  396. });
  397. const timeStart = Date.now();
  398. await runner.runCode(sourceCode);
  399. const timeEnd = Date.now();
  400. return {
  401. status: "OK",
  402. exitCode: "0",
  403. execTime: (timeEnd - timeStart),
  404. input,
  405. output: stdout,
  406. error: stderr,
  407. };
  408. });
  409.  
  410. const runners = {
  411. "4001": [new WandboxRunner("gcc-10.1.0-c", "C (GCC 10.1.0)")],
  412. "4002": [new PaizaIORunner("c", "C (C17 / Clang 10.0.0)")],
  413. "4003": [new WandboxCppRunner("gcc-10.1.0", "C++ (GCC 10.1.0)", { options: "warning,boost-1.73.0-gcc-9.2.0,gnu++17" })],
  414. "4004": [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)", { options: "warning,boost-nothing-clang-10.0.0,c++17" })],
  415. "4006": [
  416. new PaizaIORunner("python3", "Python (3.8.2)"),
  417. brythonRunner,
  418. ],
  419. "4007": [new PaizaIORunner("bash", "Bash (5.0.17)")],
  420. "4010": [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
  421. "4011": [new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)")],
  422. "4013": [new PaizaIORunner("clojure", "Clojure (1.10.1-1)")],
  423. "4017": [new PaizaIORunner("d", "D (LDC 1.23.0)")],
  424. "4020": [new PaizaIORunner("erlang", "Erlang (10.6.4)")],
  425. "4021": [new PaizaIORunner("elixir", "Elixir (1.10.4)")],
  426. "4022": [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
  427. "4023": [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
  428. "4026": [new WandboxRunner("go-1.14.1", "Go (1.14.1)")],
  429. "4027": [new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)")],
  430. "4030": [new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)")],
  431. "4032": [new PaizaIORunner("kotlin", "Kotlin (1.4.0)")],
  432. "4033": [new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)")],
  433. "4034": [new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)")],
  434. "4036": [new WandboxRunner("nim-1.0.6", "Nim (1.0.6)")],
  435. "4037": [new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)")],
  436. "4039": [new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)")],
  437. "4041": [new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)")],
  438. "4042": [new PaizaIORunner("perl", "Perl (5.30.0)")],
  439. "4044": [
  440. new PaizaIORunner("php", "PHP (7.4.10)"),
  441. new WandboxRunner("php-7.3.3", "PHP (7.3.3)"),
  442. ],
  443. "4046": [new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)")],
  444. "4047": [new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)")],
  445. "4049": [
  446. new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  447. new WandboxRunner("ruby-head", "Ruby (HEAD 3.0.0dev)"),
  448. new WandboxRunner("ruby-2.7.0-preview1", "Ruby (2.7.0-preview1)"),
  449. ],
  450. "4050": [
  451. new AtCoderRunner("4050", "Rust (1.42.0)"),
  452. new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
  453. new PaizaIORunner("rust", "Rust (1.43.0)"),
  454. ],
  455. "4051": [new PaizaIORunner("scala", "Scala (2.13.3)")],
  456. "4053": [new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)")],
  457. "4055": [new PaizaIORunner("swift", "Swift (5.2.5)")],
  458. "4056": [new CustomRunner("Text", async (sourceCode, input) => {
  459. return {
  460. status: "OK",
  461. exitCode: "0",
  462. input,
  463. output: sourceCode,
  464. };
  465. })],
  466. "4058": [new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)")],
  467. "4061": [new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)")],
  468. "4101": [new WandboxCppRunner("gcc-9.2.0", "C++ (GCC 9.2.0)")],
  469. "4102": [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)")],
  470. };
  471. for (const e of document.querySelectorAll("#select-lang option[value]")) {
  472. const languageId = e.value;
  473. // 特別な CodeRunner が定義されていない言語ID
  474. if (!(languageId in runners))
  475. runners[languageId] = [];
  476. // AtCoderRunner がない場合は、追加する
  477. if (runners[languageId].some((runner) => runner instanceof AtCoderRunner))
  478. continue;
  479. runners[languageId].push(new AtCoderRunner(languageId, e.textContent));
  480. }
  481. console.info("AtCoder Easy Test: codeRunner OK");
  482. var codeRunner = {
  483. // 指定した環境でコードを実行する
  484. run(languageId, index, sourceCode, input, expectedOutput, options = { trim: true, split: true }) {
  485. // CodeRunner が存在しない言語ID
  486. if (!(languageId in runners))
  487. return Promise.reject("Language not supported");
  488. if (!(index in runners[languageId]))
  489. return Promise.reject(`Runner index out of range: [0, ${runners[languageId].length})`);
  490. // 最後に実行したコードを保存
  491. codeSaver.save(sourceCode);
  492. // 実行
  493. return runners[languageId][index].test(sourceCode, input, expectedOutput, options);
  494. },
  495. // 環境の名前の一覧を取得する
  496. async getEnvironment(languageId) {
  497. if (!(languageId in runners))
  498. throw "language not supported";
  499. return runners[languageId].map((runner) => runner.label);
  500. },
  501. };
  502.  
  503. function getTestCases() {
  504. const selectors = [
  505. ["#task-statement p+pre.literal-block", ".section"],
  506. ["#task-statement pre.source-code-for-copy", ".part"],
  507. ["#task-statement .lang>*:nth-child(1) .div-btn-copy+pre", ".part"],
  508. ["#task-statement .div-btn-copy+pre", ".part"],
  509. ["#task-statement>.part pre.linenums", ".part"],
  510. ["#task-statement>.part:not(.io-style)>h3+section>pre", ".part"],
  511. ["#task-statement pre", ".part"],
  512. ];
  513. for (const [selector, closestSelector] of selectors) {
  514. const e = [...document.querySelectorAll(selector)].filter(e => {
  515. if (e.closest(".io-style"))
  516. return false; // practice2
  517. return true;
  518. });
  519. if (e.length == 0)
  520. continue;
  521. const testcases = [];
  522. let sampleId = 1;
  523. for (let i = 0; i < e.length; i += 2) {
  524. const container = e[i].closest(closestSelector) || e[i].parentElement;
  525. testcases.push({
  526. title: `Sample ${sampleId++}`,
  527. input: (e[i] || {}).textContent,
  528. output: (e[i + 1] || {}).textContent,
  529. anchor: container.querySelector("h3"),
  530. });
  531. }
  532. return testcases;
  533. }
  534. return [];
  535. }
  536.  
  537. function html2element(html) {
  538. const template = document.createElement("template");
  539. template.innerHTML = html;
  540. return template.content.firstChild;
  541. }
  542. const eventListeners = {};
  543. const events = {
  544. on(name, listener) {
  545. const listeners = (name in eventListeners ? eventListeners[name] : eventListeners[name] = []);
  546. listeners.push(listener);
  547. },
  548. trig(name) {
  549. if (name in eventListeners) {
  550. for (const listener of eventListeners[name])
  551. listener();
  552. }
  553. },
  554. };
  555.  
  556. var hBottomMenu = "<div id=\"bottom-menu-wrapper\" class=\"navbar navbar-default navbar-fixed-bottom\">\n <div class=\"container\">\n <div class=\"navbar-header\">\n <button id=\"bottom-menu-key\" type=\"button\" class=\"navbar-toggle collapsed glyphicon glyphicon-menu-down\" data-toggle=\"collapse\" data-target=\"#bottom-menu\"></button>\n </div>\n <div id=\"bottom-menu\" class=\"collapse navbar-collapse\">\n <ul id=\"bottom-menu-tabs\" class=\"nav nav-tabs\"></ul>\n <div id=\"bottom-menu-contents\" class=\"tab-content\"></div>\n </div>\n </div>\n</div>";
  557.  
  558. var hStyle$1 = "<style>\n#bottom-menu-wrapper {\n background: transparent;\n border: none;\n pointer-events: none;\n padding: 0;\n}\n\n#bottom-menu-wrapper>.container {\n position: absolute;\n bottom: 0;\n width: 100%;\n padding: 0;\n}\n\n#bottom-menu-wrapper>.container>.navbar-header {\n float: none;\n}\n\n#bottom-menu-key {\n display: block;\n float: none;\n margin: 0 auto;\n padding: 10px 3em;\n border-radius: 5px 5px 0 0;\n background: #000;\n opacity: 0.5;\n color: #FFF;\n cursor: pointer;\n pointer-events: auto;\n text-align: center;\n}\n\n@media screen and (max-width: 767px) {\n #bottom-menu-key {\n opacity: 0.25;\n }\n}\n\n#bottom-menu-key.collapsed:before {\n content: \"\\e260\";\n}\n\n#bottom-menu-tabs {\n padding: 3px 0 0 10px;\n cursor: n-resize;\n}\n\n#bottom-menu-tabs a {\n pointer-events: auto;\n}\n\n#bottom-menu {\n pointer-events: auto;\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n max-height: unset;\n}\n\n#bottom-menu.collapse:not(.in) {\n display: none !important;\n}\n\n#bottom-menu-tabs>li>a {\n background: rgba(150, 150, 150, 0.5);\n color: #000;\n border: solid 1px #ccc;\n filter: brightness(0.75);\n}\n\n#bottom-menu-tabs>li>a:hover {\n background: rgba(150, 150, 150, 0.5);\n border: solid 1px #ccc;\n color: #111;\n filter: brightness(0.9);\n}\n\n#bottom-menu-tabs>li.active>a {\n background: #eee;\n border: solid 1px #ccc;\n color: #333;\n filter: none;\n}\n\n.bottom-menu-btn-close {\n font-size: 8pt;\n vertical-align: baseline;\n padding: 0 0 0 6px;\n margin-right: -6px;\n}\n\n#bottom-menu-contents {\n padding: 5px 15px;\n max-height: 50vh;\n overflow-y: auto;\n}\n\n#bottom-menu-contents .panel {\n color: #333;\n}\n</style>";
  559.  
  560. const style = html2element(hStyle$1);
  561. const bottomMenu = html2element(hBottomMenu);
  562. unsafeWindow.document.head.appendChild(style);
  563. unsafeWindow.document.getElementById("main-div").appendChild(bottomMenu);
  564. const bottomMenuKey = bottomMenu.querySelector("#bottom-menu-key");
  565. const bottomMenuTabs = bottomMenu.querySelector("#bottom-menu-tabs");
  566. const bottomMenuContents = bottomMenu.querySelector("#bottom-menu-contents");
  567. // メニューのリサイズ
  568. {
  569. let resizeStart = null;
  570. const onStart = (event) => {
  571. const target = event.target;
  572. const pageY = event.pageY;
  573. if (target.id != "bottom-menu-tabs")
  574. return;
  575. resizeStart = { y: pageY, height: bottomMenuContents.getBoundingClientRect().height };
  576. };
  577. const onMove = (event) => {
  578. if (!resizeStart)
  579. return;
  580. event.preventDefault();
  581. bottomMenuContents.style.height = `${resizeStart.height - (event.pageY - resizeStart.y)}px`;
  582. };
  583. const onEnd = () => {
  584. resizeStart = null;
  585. };
  586. bottomMenuTabs.addEventListener("mousedown", onStart);
  587. bottomMenuTabs.addEventListener("mousemove", onMove);
  588. bottomMenuTabs.addEventListener("mouseup", onEnd);
  589. bottomMenuTabs.addEventListener("mouseleave", onEnd);
  590. }
  591. let tabs = new Set();
  592. let selectedTab = null;
  593. /** 下メニューの操作 */
  594. const menuController = {
  595. /** タブを選択 */
  596. selectTab(tabId) {
  597. const tab = unsafeWindow.$(`#bottom-menu-tab-${tabId}`);
  598. if (tab && tab[0]) {
  599. tab.tab("show"); // Bootstrap 3
  600. selectedTab = tabId;
  601. }
  602. },
  603. /** 下メニューにタブを追加する */
  604. addTab(tabId, tabLabel, paneContent, options = {}) {
  605. console.log(`AtCoder Easy Test: addTab: ${tabLabel} (${tabId})`, paneContent);
  606. // タブを追加
  607. const tab = document.createElement("a");
  608. tab.textContent = tabLabel;
  609. tab.id = `bottom-menu-tab-${tabId}`;
  610. tab.href = "#";
  611. tab.dataset.target = `#bottom-menu-pane-${tabId}`;
  612. tab.dataset.toggle = "tab";
  613. tab.addEventListener("click", event => {
  614. event.preventDefault();
  615. menuController.selectTab(tabId);
  616. });
  617. const tabLi = document.createElement("li");
  618. tabLi.appendChild(tab);
  619. bottomMenuTabs.appendChild(tabLi);
  620. // 内容を追加
  621. const pane = document.createElement("div");
  622. pane.className = "tab-pane";
  623. pane.id = `bottom-menu-pane-${tabId}`;
  624. pane.appendChild(paneContent);
  625. bottomMenuContents.appendChild(pane);
  626. const controller = {
  627. get id() {
  628. return tabId;
  629. },
  630. close() {
  631. bottomMenuTabs.removeChild(tabLi);
  632. bottomMenuContents.removeChild(pane);
  633. tabs.delete(tab);
  634. if (selectedTab == tabId) {
  635. selectedTab = null;
  636. if (tabs.size > 0) {
  637. menuController.selectTab(tabs.values().next().value.id);
  638. }
  639. }
  640. },
  641. show() {
  642. menuController.show();
  643. menuController.selectTab(tabId);
  644. },
  645. set color(color) {
  646. tab.style.backgroundColor = color;
  647. },
  648. };
  649. // 選択されているタブがなければ選択
  650. if (!selectedTab)
  651. menuController.selectTab(tabId);
  652. return controller;
  653. },
  654. /** 下メニューを表示する */
  655. show() {
  656. if (bottomMenuKey.classList.contains("collapsed"))
  657. bottomMenuKey.click();
  658. },
  659. /** 下メニューの表示/非表示を切り替える */
  660. toggle() {
  661. bottomMenuKey.click();
  662. },
  663. };
  664. console.info("AtCoder Easy Test: bottomMenu OK");
  665.  
  666. var hRowTemplate = "<div class=\"atcoder-easy-test-cases-row alert alert-dismissible\">\n <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"close\">\n <span aria-hidden=\"true\">×</span>\n </button>\n <div class=\"progress\">\n <div class=\"progress-bar\" style=\"width: 0%;\">0 / 0</div>\n </div>\n <!--div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div>\n <div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div>\n <div class=\"label label-default label-warning\" style=\"margin: 3px; cursor: pointer;\">WA</div-->\n</div>";
  667.  
  668. class ResultRow {
  669. _tabs;
  670. _element;
  671. _promise;
  672. constructor(pairs) {
  673. this._tabs = pairs.map(([_, tab]) => tab);
  674. this._element = html2element(hRowTemplate);
  675. const numCases = pairs.length;
  676. let numFinished = 0;
  677. let numAccepted = 0;
  678. const progressBar = this._element.querySelector(".progress-bar");
  679. progressBar.textContent = `${numFinished} / ${numCases}`;
  680. this._promise = Promise.all(pairs.map(([pResult, tab]) => {
  681. const button = html2element(`<div class="label label-default" style="margin: 3px; cursor: pointer;">WJ</div>`);
  682. button.addEventListener("click", () => {
  683. tab.show();
  684. });
  685. this._element.appendChild(button);
  686. return pResult.then(result => {
  687. button.textContent = result.status;
  688. if (result.status == "AC") {
  689. button.classList.add("label-success");
  690. }
  691. else if (result.status != "OK") {
  692. button.classList.add("label-warning");
  693. }
  694. numFinished++;
  695. if (result.status == "AC")
  696. numAccepted++;
  697. progressBar.textContent = `${numFinished} / ${numCases}`;
  698. progressBar.style.width = `${100 * numFinished / numCases}%`;
  699. if (numFinished == numCases) {
  700. if (numAccepted == numCases)
  701. this._element.classList.add("alert-success");
  702. else
  703. this._element.classList.add("alert-warning");
  704. }
  705. }).catch(reason => {
  706. button.textContent = "IE";
  707. button.classList.add("label-danger");
  708. console.error(reason);
  709. });
  710. }));
  711. }
  712. get element() {
  713. return this._element;
  714. }
  715. onFinish(listener) {
  716. this._promise.then(listener);
  717. }
  718. remove() {
  719. for (const tab of this._tabs)
  720. tab.close();
  721. const parent = this._element.parentElement;
  722. if (parent)
  723. parent.removeChild(this._element);
  724. }
  725. }
  726.  
  727. var hResultList = "<div class=\"row\"></div>";
  728.  
  729. const eResultList = html2element(hResultList);
  730. unsafeWindow.document.querySelector(".form-code-submit").appendChild(eResultList);
  731. const resultList = {
  732. addResult(pairs) {
  733. const result = new ResultRow(pairs);
  734. eResultList.appendChild(result.element);
  735. return result;
  736. },
  737. };
  738.  
  739. var hTabTemplate = "<div class=\"atcoder-easy-test-result container\">\n <div class=\"row\">\n <div class=\"atcoder-easy-test-result-col-input col-xs-12\" data-if-expected-output=\"col-sm-6 col-sm-push-6\">\n <div class=\"form-group\">\n <label class=\"control-label col-xs-12\">\n Standard Input\n <div class=\"col-xs-12\">\n <textarea class=\"atcoder-easy-test-result-input form-control\" rows=\"3\" readonly=\"readonly\"></textarea>\n </div>\n </label>\n </div>\n </div>\n <div class=\"atcoder-easy-test-result-col-expected-output col-xs-12 col-sm-6 hidden\" data-if-expected-output=\"!hidden col-sm-pull-6\">\n <div class=\"form-group\">\n <label class=\"control-label col-xs-12\">\n Expected Output\n <div class=\"col-xs-12\">\n <textarea class=\"atcoder-easy-test-result-expected-output form-control\" rows=\"3\" readonly=\"readonly\"></textarea>\n </div>\n </label>\n </div>\n </div>\n </div>\n <div class=\"row\"><div class=\"col-sm-6 col-sm-offset-3\">\n <div class=\"panel panel-default\">\n <table class=\"table table-condensed\">\n <tbody>\n <tr>\n <th class=\"text-center\">Exit Code</th>\n <th class=\"text-center\">Exec Time</th>\n <th class=\"text-center\">Memory</th>\n </tr>\n <tr>\n <td class=\"atcoder-easy-test-result-exit-code text-center\"></td>\n <td class=\"atcoder-easy-test-result-exec-time text-center\"></td>\n <td class=\"atcoder-easy-test-result-memory text-center\"></td>\n </tr>\n </tbody>\n </table>\n </div>\n </div></div>\n <div class=\"row\">\n <div class=\"atcoder-easy-test-result-col-output col-xs-12\" data-if-error=\"col-md-6\">\n <div class=\"form-group\">\n <label class=\"control-label col-xs-12\">\n Standard Output\n <div class=\"col-xs-12\">\n <textarea class=\"atcoder-easy-test-result-output form-control\" rows=\"5\" readonly=\"readonly\"></textarea>\n </div>\n </label>\n </div>\n </div>\n <div class=\"atcoder-easy-test-result-col-error col-xs-12 col-md-6 hidden\" data-if-error=\"!hidden\">\n <div class=\"form-group\">\n <label class=\"control-label col-xs-12\">\n Standard Error\n <div class=\"col-xs-12\">\n <textarea class=\"atcoder-easy-test-result-error form-control\" rows=\"5\" readonly=\"readonly\"></textarea>\n </div>\n </label>\n </div>\n </div>\n </div>\n</div>";
  740.  
  741. function setClassFromData(element, name) {
  742. const classes = element.dataset[name].split(/\s+/);
  743. for (let className of classes) {
  744. let flag = true;
  745. if (className[0] == "!") {
  746. className = className.slice(1);
  747. flag = false;
  748. }
  749. element.classList.toggle(className, flag);
  750. }
  751. }
  752. class ResultTabContent {
  753. _title;
  754. _uid;
  755. _element;
  756. _result;
  757. constructor() {
  758. this._uid = Date.now().toString(16);
  759. this._result = null;
  760. this._element = html2element(hTabTemplate);
  761. this._element.id = `atcoder-easy-test-result-${this._uid}`;
  762. }
  763. set result(result) {
  764. this._result = result;
  765. if (result.status == "AC") {
  766. this.outputStyle.backgroundColor = "#dff0d8";
  767. }
  768. else if (result.status != "OK") {
  769. this.outputStyle.backgroundColor = "#fcf8e3";
  770. }
  771. this.input = result.input;
  772. if ("expectedOutput" in result)
  773. this.expectedOutput = result.expectedOutput;
  774. this.exitCode = result.exitCode;
  775. if ("execTime" in result)
  776. this.execTime = `${result.execTime} ms`;
  777. if ("memory" in result)
  778. this.memory = `${result.memory} KB`;
  779. if ("output" in result)
  780. this.output = result.output;
  781. if (result.error)
  782. this.error = result.error;
  783. }
  784. get result() {
  785. return this._result;
  786. }
  787. get uid() {
  788. return this._uid;
  789. }
  790. get element() {
  791. return this._element;
  792. }
  793. set title(title) {
  794. this._title = title;
  795. }
  796. get title() {
  797. return this._title;
  798. }
  799. set input(input) {
  800. this._get("input").value = input;
  801. }
  802. get inputStyle() {
  803. return this._get("input").style;
  804. }
  805. set expectedOutput(output) {
  806. this._get("expected-output").value = output;
  807. setClassFromData(this._get("col-input"), "ifExpectedOutput");
  808. setClassFromData(this._get("col-expected-output"), "ifExpectedOutput");
  809. }
  810. get expectedOutputStyle() {
  811. return this._get("expected-output").style;
  812. }
  813. set output(output) {
  814. this._get("output").value = output;
  815. }
  816. get outputStyle() {
  817. return this._get("output").style;
  818. }
  819. set error(error) {
  820. this._get("error").value = error;
  821. setClassFromData(this._get("col-output"), "ifError");
  822. setClassFromData(this._get("col-error"), "ifError");
  823. }
  824. set exitCode(code) {
  825. const element = this._get("exit-code");
  826. element.textContent = code;
  827. const isSuccess = code == "0";
  828. element.classList.toggle("bg-success", isSuccess);
  829. element.classList.toggle("bg-danger", !isSuccess);
  830. }
  831. set execTime(time) {
  832. this._get("exec-time").textContent = time;
  833. }
  834. set memory(memory) {
  835. this._get("memory").textContent = memory;
  836. }
  837. _get(name) {
  838. return this._element.querySelector(`.atcoder-easy-test-result-${name}`);
  839. }
  840. }
  841.  
  842. var hRoot = "<form id=\"atcoder-easy-test-container\" class=\"form-horizontal\">\n <small style=\"position: absolute; display: block; bottom: 0; right: 0; padding: 1% 4%; width: 95%; text-align: right;\">AtCoder Easy Test v<span id=\"atcoder-easy-test-version\"></span></small>\n <div class=\"row\">\n <div class=\"col-xs-12 col-lg-8\">\n <div class=\"form-group\">\n <label class=\"control-label col-sm-2\">Test Environment</label>\n <div class=\"col-sm-10\">\n <select class=\"form-control\" id=\"atcoder-easy-test-language\"></select>\n </div>\n </div>\n <div class=\"form-group\">\n <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-input\">Standard Input</label>\n <div class=\"col-sm-10\">\n <textarea id=\"atcoder-easy-test-input\" name=\"input\" class=\"form-control\" rows=\"3\"></textarea>\n </div>\n </div>\n </div>\n <div class=\"col-xs-12 col-lg-4\">\n <details close>\n <summary>Expected Output</summary>\n <div class=\"form-group\">\n <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-allowable-error-check\">Allowable Error</label>\n <div class=\"col-sm-10\">\n <div class=\"input-group\">\n <span class=\"input-group-addon\">\n <input id=\"atcoder-easy-test-allowable-error-check\" type=\"checkbox\" checked=\"checked\">\n </span>\n <input id=\"atcoder-easy-test-allowable-error\" type=\"text\" class=\"form-control\" value=\"1e-6\">\n </div>\n </div>\n </div>\n <div class=\"form-group\">\n <label class=\"control-label col-sm-2\" for=\"atcoder-easy-test-output\">Expected Output</label>\n <div class=\"col-sm-10\">\n <textarea id=\"atcoder-easy-test-output\" name=\"output\" class=\"form-control\" rows=\"3\"></textarea>\n </div>\n </div>\n </details>\n </div>\n <div class=\"col-xs-12\">\n <div class=\"col-xs-11 col-xs-offset=1\">\n <div class=\"form-group\">\n <a id=\"atcoder-easy-test-run\" class=\"btn btn-primary\">Run</a>\n </div>\n </div>\n </div>\n </div>\n <style>\n #atcoder-easy-test-language {\n border: none;\n background: transparent;\n font: inherit;\n color: #fff;\n }\n #atcoder-easy-test-language option {\n border: none;\n color: #333;\n font: inherit;\n }\n </style>\n</form>";
  843.  
  844. var hStyle = "<style>\n.atcoder-easy-test-result textarea {\n font-family: monospace;\n font-weight: normal;\n}\n</style>";
  845.  
  846. var hTestAndSubmit = "<a id=\"atcoder-easy-test-btn-test-and-submit\" class=\"btn btn-info btn\" style=\"margin-left: 1rem\" title=\"Ctrl+Enter\" data-toggle=\"tooltip\">Test &amp; Submit</a>";
  847.  
  848. var hTestAllSamples = "<a id=\"atcoder-easy-test-btn-test-all\" class=\"btn btn-default btn-sm\" style=\"margin-left: 1rem\" title=\"Alt+Enter\" data-toggle=\"tooltip\">Test All Samples</a>";
  849.  
  850. const doc = unsafeWindow.document;
  851. const $ = unsafeWindow.$;
  852. const $select = (selector) => doc.querySelector(selector);
  853. // external interfaces
  854. unsafeWindow.bottomMenu = menuController;
  855. unsafeWindow.codeRunner = codeRunner;
  856. doc.head.appendChild(html2element(hStyle));
  857. // place "Easy Test" tab
  858. {
  859. const eAtCoderLang = $select("#select-lang>select");
  860. const eSubmitButton = doc.getElementById("submit");
  861. // declare const hRoot: string;
  862. const root = html2element(hRoot);
  863. const E = (id) => root.querySelector(`#atcoder-easy-test-${id}`);
  864. const eLanguage = E("language");
  865. const eInput = E("input");
  866. const eAllowableErrorCheck = E("allowable-error-check");
  867. const eAllowableError = E("allowable-error");
  868. const eOutput = E("output");
  869. const eRun = E("run");
  870. E("version").textContent = "2.0.1";
  871. events.on("enable", () => {
  872. eRun.classList.remove("disabled");
  873. });
  874. events.on("disable", () => {
  875. eRun.classList.remove("enabled");
  876. });
  877. // 言語選択関係
  878. {
  879. async function setLanguage() {
  880. const languageId = eAtCoderLang.value;
  881. while (eLanguage.firstChild)
  882. eLanguage.removeChild(eLanguage.firstChild);
  883. try {
  884. const labels = await codeRunner.getEnvironment(languageId);
  885. console.log(`language: ${labels[0]} (${languageId})`);
  886. labels.forEach((label, index) => {
  887. const option = document.createElement("option");
  888. option.value = String(index);
  889. option.textContent = label;
  890. eLanguage.appendChild(option);
  891. });
  892. events.trig("enable");
  893. }
  894. catch (error) {
  895. console.log(`language: ? (${languageId})`);
  896. console.error(error);
  897. const option = document.createElement("option");
  898. option.className = "fg-danger";
  899. option.textContent = error;
  900. eLanguage.appendChild(option);
  901. events.trig("disable");
  902. }
  903. }
  904. unsafeWindow.$(eAtCoderLang).change(() => setLanguage()); //NOTE: This event is only for jQuery; do not replace with Vanilla
  905. eAllowableError.disabled = !eAllowableErrorCheck.checked;
  906. eAllowableErrorCheck.addEventListener("change", event => {
  907. eAllowableError.disabled = !eAllowableErrorCheck.checked;
  908. });
  909. setLanguage();
  910. }
  911. let runId = 0;
  912. // テスト実行
  913. function runTest(title, input, output = null) {
  914. runId++;
  915. events.trig("disable");
  916. const options = { trim: true, split: true, };
  917. if (eAllowableErrorCheck.checked) {
  918. options.allowableError = parseFloat(eAllowableError.value);
  919. }
  920. const content = new ResultTabContent();
  921. const tab = menuController.addTab("easy-test-result-" + content.uid, `#${runId} ${title}`, content.element, { active: true, closeButton: true });
  922. const pResult = codeRunner.run(eAtCoderLang.value, +eLanguage.value, unsafeWindow.getSourceCode(), input, output, options);
  923. pResult.then(result => {
  924. content.result = result;
  925. if (result.status == "AC") {
  926. tab.color = "#dff0d8";
  927. }
  928. else if (result.status != "OK") {
  929. tab.color = "#fcf8e3";
  930. }
  931. events.trig("enable");
  932. });
  933. return [pResult, tab];
  934. }
  935. function runAllCases(testcases) {
  936. const pairs = testcases.map(testcase => runTest(testcase.title, testcase.input, testcase.output));
  937. resultList.addResult(pairs);
  938. return Promise.all(pairs.map(([pResult, _]) => pResult.then(result => {
  939. if (result.status == "AC")
  940. return Promise.resolve(result);
  941. else
  942. return Promise.reject(result);
  943. })));
  944. }
  945. eRun.addEventListener("click", _ => {
  946. const title = "Run";
  947. const input = eInput.value;
  948. const output = eOutput.value;
  949. runTest(title, input, output || null);
  950. });
  951. menuController.addTab("easy-test", "Easy Test", root);
  952. // place "Test & Submit" button
  953. {
  954. const button = html2element(hTestAndSubmit);
  955. eSubmitButton.parentElement.appendChild(button);
  956. button.addEventListener("click", async () => {
  957. await runAllCases(getTestCases());
  958. eSubmitButton.click();
  959. });
  960. }
  961. // place "Test All Samples" button
  962. {
  963. const button = html2element(hTestAllSamples);
  964. eSubmitButton.parentElement.appendChild(button);
  965. button.addEventListener("click", () => runAllCases(getTestCases()));
  966. }
  967. }
  968. // place "Restore Last Play" button
  969. try {
  970. const restoreButton = doc.createElement("a");
  971. restoreButton.className = "btn btn-danger btn-sm";
  972. restoreButton.textContent = "Restore Last Play";
  973. restoreButton.addEventListener("click", async () => {
  974. try {
  975. const lastCode = await codeSaver.restore();
  976. if (confirm("Your current code will be replaced. Are you sure?")) {
  977. $select(".plain-textarea").value = lastCode;
  978. $(".editor").data("editor").doc.setValue(lastCode);
  979. }
  980. }
  981. catch (reason) {
  982. alert(reason);
  983. }
  984. });
  985. $select(".editor-buttons").appendChild(restoreButton);
  986. }
  987. catch (e) {
  988. console.error(e);
  989. }
  990. })();