Greasy Fork 还支持 简体中文。

AtCoder Editorial for Typical90

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

  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. }