AtCoder Editorial for Typical90

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

Від 06.06.2021. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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