AtCoder Editorial for Typical90

AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。

2021-06-06 기준 버전입니다. 최신 버전을 확인하세요.

  1. // ==UserScript==
  2. // @name AtCoder Editorial for Typical90
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.0
  5. // @description AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。
  6. // @match https://atcoder.jp/contests/typical90*
  7. // @require https://code.jquery.com/jquery-3.6.0.min.js
  8. // @author hiro_hiro
  9. // @license CC0
  10. // @downloadURL
  11. // @updateURL
  12. // @supportURL
  13. // @grant GM_addStyle
  14. // ==/UserScript==
  15.  
  16. (async function () {
  17. "use strict";
  18.  
  19. addTabContentStyles();
  20. addEditorialTab();
  21. const tasks = await fetchTasks(); // TODO: Use cache to reduce access to AtCoder.
  22. addEditorialPage(tasks);
  23.  
  24. $(".nav-tabs a").click(function () {
  25. changeTab($(this));
  26. hideContentsOfPreviousPage();
  27.  
  28. return false;
  29. });
  30.  
  31. // TODO: 「解説」ボタンをクリックしたら、該当する問題のリンクを表示できるようにする
  32. })();
  33.  
  34. function addTabContentStyles() {
  35. const tabContentStyles = `
  36. .tabContent {
  37. display: none;
  38. }
  39. .tabContent.active {
  40. display: block;
  41. }
  42. `;
  43.  
  44. GM_addStyle(tabContentStyles);
  45. }
  46.  
  47. // FIXME: Hard coding is not good.
  48. function addEditorialTab() {
  49. // See:
  50. // https://api.jquery.com/before/
  51. $("li.pull-right").before("<li><a href='#editorial-created-by-userscript'><span class='glyphicon glyphicon-book' style='margin-right:4px;' aria-hidden='true'></span>解説</a></li>");
  52. }
  53.  
  54. // TODO: キャッシュを利用して、本家へのアクセスを少なくなるようにする
  55. async function fetchTasks() {
  56. const tbodies = await fetchTaskPage();
  57. const tasks = new Object();
  58. let taskCount = 1;
  59.  
  60. for (const [index, aTag] of Object.entries($(tbodies).find("a"))) {
  61. // Ignore a-tags including task-id and "Submit".
  62. if (index % 3 == 1) {
  63. const taskId = String(taskCount).padStart(3, "0");
  64. tasks[taskId] = [aTag.text, aTag.href];
  65. taskCount += 1;
  66. }
  67. }
  68.  
  69. return tasks;
  70. }
  71.  
  72. async function fetchTaskPage() {
  73. // See:
  74. // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  75. // https://developer.mozilla.org/en-US/docs/Web/API/Body/text
  76. // https://developer.mozilla.org/ja/docs/Web/API/DOMParser
  77. // https://api.jquery.com/each/
  78. // http://dyn-web.com/tutorials/object-literal/properties.php#:~:text=Add%20a%20Property%20to%20an%20Existing%20Object%20Literal&text=myObject.,if%20it%20is%20a%20string).
  79. const tbodies = await fetch("https://atcoder.jp/contests/typical90/tasks", {
  80. method: "GET"
  81. })
  82. .then(response => {
  83. return response.text()
  84. })
  85. .then(html => {
  86. const parser = new DOMParser();
  87. const doc = parser.parseFromString(html, "text/html");
  88. const messages = doc.querySelector("#main-container > div.row > div:nth-child(2) > div > table > tbody");
  89.  
  90. return messages;
  91. })
  92. .catch(error => {
  93. console.warn('Something went wrong.', error);
  94. });
  95.  
  96. return tbodies;
  97. }
  98.  
  99. function addEditorialPage(tasks) {
  100. addTabContent();
  101.  
  102. const editorialId = "#editorial-created-by-userscript";
  103.  
  104. showHeader(editorialId);
  105. addHorizontalRule(editorialId);
  106. showDifficultyVotingAndUserCodes(editorialId);
  107.  
  108. let taskEditorialsDiv = addDiv("task-editorials", editorialId);
  109. taskEditorialsDiv = "." + taskEditorialsDiv;
  110. addEditorials(tasks, taskEditorialsDiv);
  111. }
  112.  
  113. function addTabContent() {
  114. const contestNavTabsId = document.getElementById("contest-nav-tabs");
  115.  
  116. // See:
  117. // https://stackoverflow.com/questions/268490/jquery-document-createelement-equivalent
  118. // https://blog.toshimaru.net/jqueryhidden-inputjquery/
  119. $("<div>", {
  120. class: "tabContent",
  121. id: "editorial-created-by-userscript",
  122. }).appendTo(contestNavTabsId);
  123. }
  124.  
  125. function showHeader(tag) {
  126. addHeader(
  127. "<h2>", // heading_tag
  128. "editorial-header", // className
  129. "解説", // text
  130. tag // parent_tag
  131. );
  132. }
  133.  
  134. function addHeader(heading_tag, className, text, parent_tag) {
  135. $(heading_tag, {
  136. class: className,
  137. text: text,
  138. }).appendTo(parent_tag);
  139. }
  140.  
  141. function addHorizontalRule(tag) {
  142. // See:
  143. // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr
  144. $("<hr>", {
  145. class: "",
  146. }).appendTo(tag);
  147. }
  148.  
  149. function showDifficultyVotingAndUserCodes(tag) {
  150. addHeader(
  151. "<h3>", // heading_tag
  152. "difficulty-voting-and-user-codes", // className
  153. "問題の難易度を投票する・ソースコードを共有する", // text
  154. tag // parent_tag
  155. );
  156.  
  157. $("<ul>", {
  158. class: "spread-sheets-ul",
  159. text: ""
  160. }).appendTo(tag);
  161.  
  162. const spreadSheetUrl = "https://docs.google.com/spreadsheets/d/1GG4Higis4n4GJBViVltjcbuNfyr31PzUY_ZY1zh2GuI/edit#gid=";
  163.  
  164. const homeID = "0";
  165. addSpreadSheetHomeURL(spreadSheetUrl + homeID);
  166.  
  167. const difficultyVotingID = "1593175261";
  168. addDifficultyVotingURL(spreadSheetUrl + difficultyVotingID);
  169.  
  170. const taskGroups = [
  171. ["001", "023", spreadSheetUrl + "105162261"], // task start, task end, spread sheet id.
  172. ["024", "047", spreadSheetUrl + "1671161250"],
  173. ["048", "071", spreadSheetUrl + "671876031"],
  174. ["072", "090", spreadSheetUrl + "428850451"]
  175. ];
  176.  
  177. // See:
  178. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
  179. taskGroups.forEach(
  180. taskGroup => {
  181. const taskStart = taskGroup[0];
  182. const taskEnd = taskGroup[1];
  183. const url = taskGroup[2];
  184.  
  185. addUserCodesURL(
  186. taskStart,
  187. taskEnd,
  188. url
  189. );
  190. }
  191. );
  192. }
  193.  
  194. function addSpreadSheetHomeURL(url) {
  195. $("<li>", {
  196. class: "spread-sheet-home-li",
  197. text: ""
  198. }).appendTo(".spread-sheets-ul");
  199.  
  200. $("<a>", {
  201. class: "spread-sheet-home-url",
  202. href: url,
  203. text: "目的",
  204. target: "_blank",
  205. rel: "noopener",
  206. }).appendTo(".spread-sheet-home-li");
  207. }
  208.  
  209. function addDifficultyVotingURL(url) {
  210. $("<li>", {
  211. class: "difficulty-voting-li",
  212. text: ""
  213. }).appendTo(".spread-sheets-ul");
  214.  
  215. $("<a>", {
  216. class: "difficulty-voting-url",
  217. href: url,
  218. text: "問題の難易度を投票する",
  219. target: "_blank",
  220. rel: "noopener",
  221. }).appendTo(".difficulty-voting-li");
  222. }
  223.  
  224. function addUserCodesURL(taskStart, taskEnd, url) {
  225. // See:
  226. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals
  227. $("<li>", {
  228. class: `user-codes-${taskStart}-${taskEnd}-li`,
  229. text: ""
  230. }).appendTo(".spread-sheets-ul");
  231.  
  232. $("<a>", {
  233. class: `user-codes-${taskStart}-${taskEnd}-url`,
  234. href: url,
  235. text: `ソースコード(${taskStart}〜${taskEnd})を見る・共有する`,
  236. target: "_blank",
  237. rel: "noopener",
  238. }).appendTo(`.user-codes-${taskStart}-${taskEnd}-li`);
  239. }
  240.  
  241. function addDiv(tagName, parentTag) {
  242. $("<div>", {
  243. class: tagName,
  244. }).appendTo(parentTag);
  245.  
  246. return tagName;
  247. }
  248.  
  249. function addEditorials(tasks, parentTag) {
  250. const githubRepoUrl = "https://github.com/E869120/kyopro_educational_90/blob/main/";
  251. const editorialsUrl = githubRepoUrl + "editorial/";
  252. const codesUrl = githubRepoUrl + "sol/";
  253.  
  254. // See:
  255. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
  256. const latestTaskId = Object.keys(tasks).slice(-1)[0];
  257.  
  258. // HACK: 公開当日分の問題についてはリンク切れを回避するため、解説・ソースコードの一覧を示すことで応急的に対処
  259. // HACK: 問題によっては、複数の解説とソースコードが公開される日もある
  260. // getMultipleEditorialUrlsIfNeeds()とgetMultipleCodeUrls()で、アドホック的に対処している
  261. for (const [taskId, [taskName, taskUrl]] of Object.entries(tasks)) {
  262. let taskEditorialDiv = addDiv(`task-${taskId}-editorial`, parentTag);
  263. taskEditorialDiv = "." + taskEditorialDiv;
  264.  
  265. // See:
  266. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
  267. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
  268. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split
  269. showTaskName(taskId, `${taskId} - ${taskName}`, taskUrl, taskEditorialDiv);
  270.  
  271. if (taskId == latestTaskId) {
  272. const message = "注: 閲覧する時間帯によっては、公式解説・想定ソースコードが公開されているかもしれません。しばらくお待ちください。";
  273. const additionalUrl = "(一覧)";
  274. addNote(message, taskEditorialDiv);
  275. showEditorial(taskId, editorialsUrl, additionalUrl, taskEditorialDiv);
  276. showCode(taskId, codesUrl, additionalUrl, taskEditorialDiv);
  277. } else {
  278. const additionalUrls = getMultipleEditorialUrlsIfNeeds(taskId);
  279.  
  280. // TODO: AtCoderの解説ページで図を表示できるようにする
  281. for (const [index, additionalUrl] of Object.entries(additionalUrls)) {
  282. const editorialUrl = editorialsUrl + taskId + additionalUrl + ".jpg";
  283. showEditorial(taskId + additionalUrl, editorialUrl, additionalUrl, taskEditorialDiv);
  284. }
  285.  
  286. const codeUrls = getMultipleCodeUrls(taskId);
  287.  
  288. // TODO: ソースコードをフォーマットされた状態で表示する
  289. for (const [index, codeUrl] of Object.entries(codeUrls)) {
  290. const editorialCodelUrl = codesUrl + taskId + codeUrl;
  291. const [additionalUrl, language] = codeUrl.split(".");
  292. showCode(taskId + additionalUrl, editorialCodelUrl, codeUrl, taskEditorialDiv);
  293. }
  294. }
  295. }
  296. }
  297.  
  298. function showTaskName(taskId, taskName, taskUrl, tag) {
  299. const taskIdClass = `task-${taskId}`;
  300.  
  301. addHeader(
  302. "<h3>", // heading_tag
  303. taskIdClass, // className
  304. taskName, // text
  305. tag // parent_tag
  306. );
  307.  
  308. $("<a>", {
  309. class: `${`task-${taskId}-url`} small glyphicon glyphicon-new-window`,
  310. href: taskUrl,
  311. target: "_blank",
  312. }).appendTo(`.${taskIdClass}`);
  313. }
  314.  
  315. // TODO: 複数の解説資料がアップロードされた日があれば更新する
  316. function getMultipleEditorialUrlsIfNeeds(taskId) {
  317. // See:
  318. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects
  319. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Property_Accessors
  320. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in
  321.  
  322. // タスク名: 解説ファイルの番号
  323. // 0xx-yyy.jpgの0xxをキーに、-yyyを値としている
  324. const multipleEditorialUrls = {
  325. "005": ["-01", "-02", "-03"],
  326. "011": ["-01", "-02"],
  327. "017": ["-01", "-02", "-03"],
  328. "023": ["-01", "-02", "-03", "-04"],
  329. "029": ["-01", "-02"],
  330. "035": ["-01", "-02", "-03"],
  331. "041": ["-01", "-02", "-03"],
  332. "047": ["-01", "-02"],
  333. "053": ["-01", "-02", "-03", "-04"],
  334. "059": ["-01", "-02", "-03"],
  335. };
  336.  
  337. if (taskId in multipleEditorialUrls) {
  338. return multipleEditorialUrls[taskId];
  339. } else {
  340. return [""]; // dummy
  341. }
  342. }
  343.  
  344. // TODO: 複数の想定コードがアップロードされた日があれば更新する
  345. function getMultipleCodeUrls(taskId) {
  346. // タスク名: ソースコードの番号と拡張子
  347. // 0xx-yyy.langの0xxをキーに、-yyy.langを値としている
  348. const multipleCodeUrls = {
  349. "005": ["-01.cpp", "-02.cpp", "-03.cpp"],
  350. "011": ["-01.cpp", "-02.cpp", "-03.cpp"],
  351. "017": ["-01.cpp", "-02.cpp", "-03.cpp"],
  352. "023": ["-01.cpp", "-02.cpp", "-03.cpp", "-04a.cpp", "-04b.cpp"],
  353. "029": ["-01.cpp", "-02.cpp", "-03.cpp"],
  354. "035": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"],
  355. "041": ["-01a.cpp", "-01b.cpp", "-02.cpp", "-03.cpp"],
  356. "047": ["-01.cpp", "-02.cpp"],
  357. "053": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"],
  358. "055": [".cpp", "-02.py", "-03.py"],
  359. "059": ["-01.cpp", "-02.cpp"],
  360. };
  361.  
  362. if (taskId in multipleCodeUrls) {
  363. return multipleCodeUrls[taskId];
  364. } else {
  365. return [".cpp"];
  366. }
  367. }
  368.  
  369. function addNote(message, parent_tag) {
  370. $("<p>", {
  371. class: "no-editorial",
  372. text: message,
  373. }).appendTo(parent_tag);
  374. }
  375.  
  376. function showEditorial(taskId, url, additionalUrl, tag) {
  377. const ulClass = `editorial-${taskId}-ul`;
  378. const liClass = `editorial-${taskId}-li`;
  379.  
  380. $("<ul>", {
  381. class: ulClass,
  382. text: ""
  383. }).appendTo(tag);
  384.  
  385. $("<li>", {
  386. class: liClass,
  387. text: ""
  388. }).appendTo(`.${ulClass}`);
  389.  
  390. $("<a>", {
  391. class: `editorial-${taskId}-url`,
  392. href: url,
  393. text: `公式解説${additionalUrl}`,
  394. target: "_blank",
  395. rel: "noopener",
  396. }).appendTo(`.${liClass}`);
  397. }
  398.  
  399. function showCode(taskId, url, additionalUrl, tag) {
  400. const ulClass = `editorial-${taskId}-code-ul`;
  401. const liClass = `editorial-${taskId}-code-li`;
  402.  
  403. $("<ul>", {
  404. class: ulClass,
  405. text: ""
  406. }).appendTo(tag);
  407.  
  408. $("<li>", {
  409. class: liClass,
  410. text: ""
  411. }).appendTo(`.${ulClass}`);
  412.  
  413. $("<a>", {
  414. class: `editorial-${taskId}-code-url`,
  415. href: url,
  416. text: `想定ソースコード${additionalUrl}`,
  417. target: "_blank",
  418. rel: "noopener",
  419. }).appendTo(`.${liClass}`);
  420. }
  421.  
  422. function addEditorialButtonToTaskPage() {
  423. // See:
  424. // https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
  425. // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
  426. const editorialButton = document.createElement("a");
  427. editorialButton.classList.add("btn", "btn-default", "btn-sm");
  428. editorialButton.textContent = "解説";
  429.  
  430. const taskTitle = document.querySelector(".row > div > .h2");
  431.  
  432. if (taskTitle) {
  433. taskTitle.appendChild(editorialButton);
  434. return editorialButton;
  435. } else {
  436. return;
  437. }
  438. }
  439.  
  440. function changeTab(this_object) {
  441. // See:
  442. // https://api.jquery.com/parent/
  443. // https://api.jquery.com/addClass/#addClass-className
  444. // https://api.jquery.com/siblings/#siblings-selector
  445. // https://api.jquery.com/removeClass/#removeClass-className
  446. // https://www.design-memo.com/coding/jquery-tab-change
  447. this_object.parent().addClass("active").siblings(".active").removeClass("active");
  448. const tabContentsUrl = this_object.attr("href");
  449. $(tabContentsUrl).addClass("active").siblings(".active").removeClass("active");
  450. }
  451.  
  452. function hideContentsOfPreviousPage() {
  453. // See:
  454. // https://api.jquery.com/length/
  455. // https://api.jquery.com/hide/
  456. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for
  457. // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String
  458. const tagCount = $(".col-sm-12").length;
  459.  
  460. for (let index = 0; index < tagCount; index++) {
  461. if (index != 0) {
  462. $("#main-container > div.row > div:nth-child(" + String(index + 1) + ")").hide();
  463. }
  464. }
  465. }