AtCoder Editorial for Typical90

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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