Tabbed AtCoder Editorial

display atcoder editorial in tabs

  1. // ==UserScript==
  2. // @name Tabbed AtCoder Editorial
  3. // @version 0.10
  4. // @description display atcoder editorial in tabs
  5. // @match https://atcoder.jp/contests/*/editorial
  6. // @match https://atcoder.jp/contests/*/editorial?*
  7. // @namespace https://greasyfork.org/users/808669
  8. // ==/UserScript==
  9.  
  10. /* jshint esversion:8 */
  11. (async function () {
  12. "use strict";
  13.  
  14. const katexoption = {
  15. delimiters: [
  16. { left: "$$", right: "$$", display: true },
  17. { left: "$", right: "$", display: false },
  18. { left: "\\(", right: "\\)", display: false },
  19. { left: "\\[", right: "\\]", display: true }
  20. ],
  21. ignoredTags: ["script", "noscript", "style", "textarea", "code", "option"],
  22. ignoredClasses: ["prettyprint", "source-code-for-copy"],
  23. throwOnError: false
  24. };
  25.  
  26. async function addScript(src) {
  27. return new Promise((resolve) => {
  28. const script = document.createElement("script");
  29. script.type = "text/javascript";
  30. script.src = src;
  31. script.onload = resolve;
  32. document.getElementsByTagName("head")[0].appendChild(script);
  33. });
  34. }
  35.  
  36. async function addLink(href) {
  37. return new Promise((resolve) => {
  38. const link = document.createElement("link");
  39. link.rel = "stylesheet";
  40. link.href = href;
  41. link.onload = resolve;
  42. document.getElementsByTagName("head")[0].appendChild(link);
  43. });
  44. }
  45.  
  46. async function getEditorial(link) {
  47. return new Promise((resolve) => {
  48. const parser = new DOMParser();
  49. const parse = s => parser.parseFromString(s, "text/html").body.firstChild;
  50. const labelNode = parse(`<label class="--editorial-title" editorial-content-state="hidden"></label>`);
  51. const handleNode = parse(`<div class="--editorial-content-handle"></div>`);
  52. const parent = link.parentNode;
  53. labelNode.replaceChildren(...parent.children);
  54. parent.appendChild(labelNode);
  55. const xhr = new XMLHttpRequest();
  56. xhr.responseType = "document";
  57. xhr.onload = (response) => {
  58. const contentNode = response.target.responseXML.querySelector("#main-container > div.row > div:nth-child(2) > div");
  59. if (contentNode) {
  60. renderMathInElement(contentNode, katexoption);
  61. contentNode.classList.add("--editorial-content");
  62. parent.appendChild(contentNode);
  63. parent.appendChild(handleNode);
  64. labelNode.addEventListener("click", () => {
  65. const state = labelNode.getAttribute("editorial-content-state");
  66. const nextState = state === "max" ? "hidden" : "max";
  67. const nextHeight = nextState === "max" ? contentNode.scrollHeight : 0;
  68. contentNode.style.setProperty("--editorial-content-height", `${nextHeight}px`);
  69. labelNode.setAttribute("editorial-content-state", nextState);
  70. });
  71. {
  72. let posY = 0;
  73. const resize = event => {
  74. const state = labelNode.getAttribute("editorial-content-state");
  75. const height = state === "max" ? contentNode.scrollHeight : state === "hidden" ? 0 : Number(contentNode.style.getPropertyValue("--editorial-content-height").replace("px", ""));
  76. const moveY = event.pageY - posY;
  77. let nextHeight = Math.min(Math.max(0, height + moveY), contentNode.scrollHeight);
  78. let nextState = "show";
  79. if (nextHeight >= contentNode.scrollHeight) nextState = "max";
  80. if (nextHeight === 0) nextState = "hidden";
  81. contentNode.style.setProperty("--editorial-content-height", `${nextHeight}px`);
  82. labelNode.setAttribute("editorial-content-state", nextState);
  83. if (moveY < 0) contentNode.scrollTop = Math.max(0, contentNode.scrollTop + moveY);
  84. posY = event.pageY;
  85. };
  86. handleNode.addEventListener("mousedown", event => {
  87. posY = event.pageY;
  88. event.preventDefault();
  89. document.addEventListener("mousemove", resize);
  90. });
  91. document.addEventListener("mouseup", () => document.removeEventListener("mousemove", resize));
  92. contentNode.__set_editorial_content_open = () => {
  93. contentNode.style.setProperty("--editorial-content-height", `${contentNode.scrollHeight}px`);
  94. labelNode.setAttribute("editorial-content-state", "max");
  95. };
  96. contentNode.__set_editorial_content_close = () => {
  97. contentNode.style.setProperty("--editorial-content-height", `0px`);
  98. labelNode.setAttribute("editorial-content-state", "hidden");
  99. };
  100. if (window.localStorage.getItem("tabbed-atCoder-editorial-default-open") === "1") {
  101. contentNode.__set_editorial_content_open();
  102. } else {
  103. contentNode.__set_editorial_content_close();
  104. }
  105. }
  106. }
  107. resolve();
  108. };
  109. xhr.open("GET", link.href);
  110. xhr.send();
  111. });
  112. }
  113.  
  114. async function getTextResponse(href) {
  115. return new Promise((resolve) => {
  116. const xhr = new XMLHttpRequest();
  117. xhr.onload = (response) => {
  118. resolve(response.target.responseText);
  119. };
  120. xhr.open("GET", href);
  121. xhr.overrideMimeType("text/plain; charset=Shift_JIS");
  122. xhr.send();
  123. });
  124. }
  125.  
  126. async function typical90(id) {
  127. const editorial = { "005": 3, "011": 2, "017": 3, "023": 4, "029": 2, "035": 3, "041": 3, "047": 2, "053": 4, "059": 3, "065": 3, "071": 3, "077": 3, "083": 4, "084": 2, "085": 2, "086": 2, "087": 2, "088": 2, "089": 4, "090": 6 };
  128. const source = { "005": "005-03", "011": "011-03", "017": "017-03", "023": "023-04b", "029": "029-03", "035": "035-04", "041": "041-03", "047": "047-02", "053": "053-04", "059": "059-02", "061": "061-02", "065": "065-03", "071": "071-03", "077": "077-04b", "083": "083-02a", "084": "084-02", "089": "089-05", "090": "090-07b" };
  129. let content = `<a href="https://github.com/E869120/kyopro_educational_90/blob/main/problem" rel="nofollow">問題文</a>
  130. <img src="https://raw.githubusercontent.com/E869120/kyopro_educational_90/main/problem/${id}.jpg" style="max-width: 100%;">
  131. <hr><a href="https://github.com/E869120/kyopro_educational_90/blob/main/editorial" rel="nofollow">公式解説</a>
  132. `;
  133. if (editorial[id] === undefined) {
  134. content += `<img src="https://raw.githubusercontent.com/E869120/kyopro_educational_90/main/editorial/${id}.jpg" style="max-width: 100%;">`;
  135. } else {
  136. for (let i = 1; i <= editorial[id]; i++) {
  137. content += `<img src="https://raw.githubusercontent.com/E869120/kyopro_educational_90/main/editorial/${id}-${String(i).padStart(2, "0")}.jpg" style="max-width: 100%;">`;
  138. }
  139. }
  140. const code = await getTextResponse(`https://raw.githubusercontent.com/E869120/kyopro_educational_90/main/sol/${source[id] === undefined ? id : source[id]}.cpp`);
  141. content += `<hr><a href="https://github.com/E869120/kyopro_educational_90/tree/main/sol" rel="nofollow">想定ソースコード</a><pre class="prettyprint linenums"><code class="language-cpp">${code}</code></pre>`;
  142. return `<ul><li>${content}</li></ul>`;
  143. }
  144.  
  145. async function createTab() {
  146. const parser = new DOMParser();
  147. const parse = s => parser.parseFromString(s, "text/html").body.firstChild;
  148. const nav = document.querySelector("#main-container > div.row > div:nth-child(2)");
  149. const dummy = document.createElement("div");
  150. const navul = parse(`<ul class="nav nav-tabs" role="tablist"></ul>`);
  151. const navdiv = parse(`<div class="tab-content"></div>`);
  152.  
  153. let previd = "dummy";
  154. let isactive = true;
  155. let activeid;
  156. let prevhead = -1;
  157. let kaisetsu = -1;
  158.  
  159. while (nav.children.length > 0) {
  160. const e = nav.firstChild;
  161. const summary = e.textContent.trimStart().split(/\s+/)[0];
  162. if (e.tagName === "DIV" && summary === "解説") {
  163. kaisetsu = dummy.children.length;
  164. dummy.appendChild(e);
  165. } else if (e.tagName === "DIV" || e.tagName === "H3") {
  166. const cond = e.textContent === "コンテスト全体の解説";
  167. const name = cond ? "全体" : summary;
  168. const id = cond ? "all" : summary;
  169. const li = parse(`<li role="presentation", id="--editorial-presentation-${id}">
  170. <a href="#--editorial-${id}" aria-controls="--editorial-${id}" role="tab" data-toggle="tab">${name}</a>
  171. </li>`);
  172. li.addEventListener("click", () => {
  173. Promise.all(
  174. Array.prototype.filter.call(
  175. document.querySelectorAll(`#--editorial-${id} li>a`),
  176. e => e.href.match(/https:\/\/atcoder\.jp\/contests\/.*\/editorial\//)
  177. ).map(e => getEditorial(e))
  178. ).then(() => { if (PR) PR.prettyPrint(); });
  179. }, { once: true });
  180. if (isactive) {
  181. li.classList.add("active");
  182. activeid = `--editorial-presentation-${id}`;
  183. }
  184. navul.appendChild(li);
  185. previd = id;
  186. prevhead = dummy.children.length;
  187. dummy.appendChild(e);
  188. } else if (e.tagName === "UL" || e.tagName == "P") {
  189. if (e.tagName === "UL") {
  190. for (let i = e.children.length; i-- > 0; i) {
  191. const ch = e.children[i];
  192. if (ch.tagName == "LI") {
  193. const link = dummy.children[prevhead].querySelector("a");
  194. if (link) {
  195. const found = link.href.match(/https:\/\/atcoder\.jp\/contests\/(.+)\/tasks\/(.+)/);
  196. if (found) {
  197. const contest = found[1];
  198. const task = found[2];
  199. const user = ch.querySelector("a.username").textContent;
  200. const a = parse(`<a href="/contests/${contest}/submissions?f.Task=${task}&f.Status=AC&f.User=${user}">
  201. <span aria-hidden="true" data-html="true" data-toggle="tooltip" title="${user}さんの提出を見る" class="glyphicon glyphicon-search black"></span>
  202. </a>`);
  203. ch.appendChild(a);
  204. }
  205. } else {
  206. const found = location.href.match(/https:\/\/atcoder\.jp\/contests\/([^/]+)\//);
  207. if (found) {
  208. const contest = found[1];
  209. const user = ch.querySelector("a.username").textContent;
  210. const a = parse(`<a href="/contests/${contest}/submissions?f.Status=AC&f.User=${user}">
  211. <span aria-hidden="true" data-html="true" data-toggle="tooltip" title="${user}さんの提出を見る" class="glyphicon glyphicon-search black"></span>
  212. </a>`);
  213. ch.appendChild(a);
  214. }
  215. }
  216. }
  217. e.insertBefore(document.createElement("hr"), ch);
  218. }
  219. }
  220. const div = document.createElement("div");
  221. div.role = "tabpanel";
  222. div.classList.add("tab-pane");
  223. if (isactive) div.classList.add("active");
  224. div.id = "--editorial-" + previd;
  225. div.appendChild(dummy.children[prevhead]);
  226. if (location.href.match(/https:\/\/atcoder\.jp\/contests\/typical90\/tasks\/.*\/editorial/) && 1 <= Number(previd) && Number(previd) <= 90) {
  227. div.appendChild(parse(await typical90(previd)));
  228. if (e.textContent !== "解説がまだありません。") {
  229. div.appendChild(e);
  230. } else {
  231. dummy.appendChild(e);
  232. }
  233. } else {
  234. div.appendChild(e);
  235. }
  236. navdiv.appendChild(div);
  237. isactive = false;
  238. } else {
  239. dummy.appendChild(e);
  240. }
  241. }
  242.  
  243. const frexDirection = window.localStorage.getItem("tabbed-atCoder-editorial-default-open") === "1" ? "column-reverse" : "column";
  244. const li = parse(`<li role="presentation", id="--editorial-open-presentation">
  245. <a href="#" style="display:flex;flex-direction:${frexDirection};padding:5px 10px;">
  246. <span class="glyphicon glyphicon-menu-up" aria-hidden="true"></span>
  247. <span class="glyphicon glyphicon-menu-down" aria-hidden="true"></span>
  248. </a>
  249. </li>`);
  250. li.addEventListener("click", () => {
  251. const a = li.querySelector("a");
  252. const contents = document.querySelectorAll(".--editorial-content");
  253. if (a.style.getPropertyValue("flex-direction") === "column") {
  254. window.localStorage.setItem("tabbed-atCoder-editorial-default-open", "1");
  255. [...contents].forEach(content => content.__set_editorial_content_open());
  256. a.style.setProperty("flex-direction", "column-reverse");
  257. } else {
  258. window.localStorage.setItem("tabbed-atCoder-editorial-default-open", "0");
  259. [...contents].forEach(content => content.__set_editorial_content_close());
  260. a.style.setProperty("flex-direction", "column");
  261. }
  262. });
  263. navul.appendChild(li);
  264.  
  265. if (kaisetsu >= 0) nav.appendChild(dummy.children[kaisetsu]);
  266. nav.appendChild(navul);
  267. nav.appendChild(navdiv);
  268. if (activeid) document.querySelector(`#${activeid}`).click();
  269. const css = `
  270. pre code {
  271. tab-size: 4;
  272. }
  273.  
  274. #main-container {
  275. overflow: hidden;
  276. }
  277.  
  278. .--editorial-title {
  279. -webkit-backface-visibility: hidden;
  280. backface-visibility: hidden;
  281. transform: translateZ(0);
  282. border: solid 1px #ccc;
  283. cursor: pointer;
  284. padding: .5em;
  285. display: block;
  286. font-weight: normal;
  287. text-align: -webkit-match-parent
  288. }
  289.  
  290. .--editorial-title > *::before {
  291. content: " ";
  292. }
  293.  
  294. .--editorial-title::after,
  295. .--editorial-title::before {
  296. content: "";
  297. position: absolute;
  298. right: 1em;
  299. top: 1em;
  300. width: 2px;
  301. height: 0.75em;
  302. background-color: #999;
  303. }
  304.  
  305. .--editorial-title::after {
  306. transform: rotate(90deg);
  307. }
  308.  
  309. .--editorial-content {
  310. --editorial-content-height: 0px;
  311. max-height: var(--editorial-content-height);
  312. overflow: auto;
  313. scrollbar-width: none;
  314. -ms-overflow-style: none;
  315. }
  316.  
  317. .--editorial-content::-webkit-scrollbar {
  318. display: none;
  319. }
  320.  
  321. label[editorial-content-state="max"]::before {
  322. background-color: transparent;
  323. }
  324.  
  325. label[editorial-content-state="hidden"]+.--editorial-content {
  326. max-height: 0;
  327. }
  328.  
  329. label[editorial-content-state="show"]+.--editorial-content {
  330. max-height: var(--editorial-content-height);
  331. }
  332.  
  333. label[editorial-content-state="max"]+.--editorial-content {
  334. max-height: max-content;
  335. }
  336.  
  337. .--editorial-content-handle {
  338. content: "";
  339. width: 100%;
  340. border-top: solid 1px #ccc;
  341. border-bottom: solid 1px #ccc;
  342. padding: 1px;
  343. }
  344.  
  345. .--editorial-content-handle {
  346. cursor: row-resize;
  347. }`;
  348. let style = document.createElement('style');
  349. style.innerHTML = css;
  350. document.head.appendChild(style);
  351. }
  352.  
  353. await addLink("https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css");
  354. await addScript("https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.js");
  355. await addScript("https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/contrib/auto-render.min.js");
  356. await addScript("https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js");
  357. await createTab();
  358. })();