Editorials Dropdown for AtCoder

Add a drop-down list next to the editorial buttons on AtCoder problem pages.

  1. // ==UserScript==
  2. // @name Editorials Dropdown for AtCoder
  3. // @name:ja Editorials Dropdown for AtCoder
  4. // @namespace https://github.com/roumcha/browser-extensions/tree/main/src/editorials-dropdown-for-atcoder
  5. // @version 1.1.0
  6. // @description Add a drop-down list next to the editorial buttons on AtCoder problem pages.
  7. // @description:ja AtCoder の解説ボタンの横にドロップダウンリストを追加します。
  8. // @author Roumcha
  9. // @license Creative Commons Zero v1.0 Universal
  10. // @match https://atcoder.jp/contests/*/tasks/*
  11. // @grant GM.xmlHttpRequest
  12. // @connect atcoder.jp
  13. // @run-at document-end
  14. // ==/UserScript==
  15.  
  16. "use strict";
  17. (() => {
  18. // src/editorials-dropdown-for-atcoder/editorials-dropdown.ts
  19. async function editorialsDropdown({
  20. fetchXMLDocument
  21. }) {
  22. const started = Date.now();
  23. let lang = getLanguage();
  24. const link = findEditorialsButton(document);
  25. if (link) {
  26. console.log("[EDFA] Found the target button: ", link);
  27. } else {
  28. console.log(`[EDFA] Editorials button not found.`);
  29. return;
  30. }
  31. const url2 = new URL(link.href);
  32. url2.searchParams.set("editorialLang", lang);
  33. const editorialsPageDoc = await fetchXMLDocument(
  34. url2
  35. ).catch((reason) => {
  36. console.error(`[EDFA] Failed to fetch ${link.href}: ${reason}`);
  37. return null;
  38. });
  39. if (editorialsPageDoc) {
  40. console.log(`[EDFA] Downloaded ${link.href}.`);
  41. } else {
  42. console.error(`[EDFA] ${link.href} is empty or not an XML document.`);
  43. return;
  44. }
  45. const content = createDropdownContent(editorialsPageDoc);
  46. if (content.length === 0) {
  47. console.error(`[EDFA] failed to generate the dropdown content.`);
  48. return;
  49. }
  50. const insertedElem = createDropdownAndButton(...content);
  51. link.after(insertedElem);
  52. console.log(
  53. `[EDFA] Successfully generated and inserted a drop-down list: `,
  54. insertedElem
  55. );
  56. console.log(`[EDFA] done in ${Date.now() - started} ms.`);
  57. }
  58. var translation = {
  59. editorial: {
  60. ja: "\u89E3\u8AAC",
  61. en: "editorial"
  62. },
  63. overallEditorial: {
  64. ja: "\u30B3\u30F3\u30C6\u30B9\u30C8\u5168\u4F53\u306E\u89E3\u8AAC",
  65. en: "overall editorial"
  66. }
  67. };
  68. function getLanguage() {
  69. const param = new URLSearchParams(location.search).get(
  70. "lang"
  71. );
  72. if (param) {
  73. console.log(`[EDFA] Found language '${param}' in the URL parameter.`);
  74. return param;
  75. }
  76. const cookie = document.cookie.split("; ").find((s) => s.startsWith("language="))?.split("=").at(1);
  77. if (cookie) {
  78. console.log(`[EDFA] Found language '${cookie}' in Cookie.`);
  79. return cookie;
  80. }
  81. const browser = navigator.language;
  82. if (browser == "ja") {
  83. console.log(`[EDFA] Loaded language '${browser}' from the browser.`);
  84. return "ja";
  85. }
  86. console.log(`[EDFA] Fall back to English.`);
  87. return "en";
  88. }
  89. function findEditorialsButton(root) {
  90. const res = [...root.querySelectorAll("a.btn")].filter(
  91. ({ textContent }) => textContent && Object.values(translation["editorial"]).includes(
  92. textContent.toLowerCase()
  93. )
  94. ).at(0);
  95. return res;
  96. }
  97. function createDropdownContent(editorialsPageDoc) {
  98. const res = [
  99. ...editorialsPageDoc.querySelectorAll(
  100. "#main-container > div > div:not(#contest-nav-tabs) > *"
  101. )
  102. ].filter(
  103. ({ tagName }) => ["ul", "h3", "p"].includes(tagName.toLowerCase())
  104. );
  105. if (res.length === 0) {
  106. console.error(`[EDFA] failed to find editorial lists.`);
  107. }
  108. return res;
  109. }
  110. function createDropdownAndButton(...content) {
  111. const res = document.createElement("span");
  112. res.className = "edfa-root";
  113. res.style.position = "relative";
  114. res.addEventListener("blur", () => res.classList.remove("open"));
  115. {
  116. const button = document.createElement("button");
  117. button.className = "edfa-button btn btn-default btn-sm";
  118. button.type = "button";
  119. button.title = "open editorials list";
  120. button.onclick = () => res.classList.toggle("open");
  121. res.append(button);
  122. {
  123. const caret = document.createElement("span");
  124. caret.classList.add("caret");
  125. button.append(caret);
  126. }
  127. }
  128. {
  129. const dropdown = document.createElement("div");
  130. dropdown.className = "edfa-dropdown dropdown-menu";
  131. dropdown.style.position = "absolute";
  132. dropdown.style.width = "200px";
  133. dropdown.style.padding = "8px";
  134. dropdown.style.zIndex = "998";
  135. dropdown.append(...content);
  136. res.append(dropdown);
  137. }
  138. return res;
  139. }
  140.  
  141. // src/editorials-dropdown-for-atcoder/info.ts
  142. var title = "Editorials Dropdown for AtCoder";
  143. var version = "1.1.0";
  144. var url = "https://github.com/roumcha/browser-extensions/tree/main/src/editorials-dropdown-for-atcoder";
  145. var author = "Roumcha";
  146. var userScriptHeader = `// ==UserScript==
  147. // @name ${title}
  148. // @name:ja ${title}
  149. // @namespace ${url}
  150. // @version ${version}
  151. // @description Add a drop-down list next to the editorial buttons on AtCoder problem pages.
  152. // @description:ja AtCoder \u306E\u89E3\u8AAC\u30DC\u30BF\u30F3\u306E\u6A2A\u306B\u30C9\u30ED\u30C3\u30D7\u30C0\u30A6\u30F3\u30EA\u30B9\u30C8\u3092\u8FFD\u52A0\u3057\u307E\u3059\u3002
  153. // @author ${author}
  154. // @license Creative Commons Zero v1.0 Universal
  155. // @match https://atcoder.jp/contests/*/tasks/*
  156. // @grant GM.xmlHttpRequest
  157. // @connect atcoder.jp
  158. // @run-at document-end
  159. // ==/UserScript==
  160. `;
  161.  
  162. // src/editorials-dropdown-for-atcoder/user-script.ts
  163. (async function() {
  164. console.log(`[EDFA] ${title} v${version} (UserScript) started.`);
  165. await editorialsDropdown({
  166. fetchXMLDocument: async (url2) => await GM.xmlHttpRequest({ url: url2 }).then((res) => res.responseXML)
  167. });
  168. })();
  169. })();