View All Editorials

View all editorials of the AtCoder contest in one page.

目前为 2023-08-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name View All Editorials
  3. // @name:ja 解説ぜんぶ見る
  4. // @description View all editorials of the AtCoder contest in one page.
  5. // @description:ja AtCoderコンテストの解説ページに、すべての問題の解説をまとめて表示します。
  6. // @version 1.5.0
  7. // @icon https://www.google.com/s2/favicons?domain=atcoder.jp
  8. // @match https://atcoder.jp/contests/*/editorial
  9. // @match https://atcoder.jp/contests/*/editorial?*
  10. // @grant GM_addStyle
  11. // @require https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.js
  12. // @require https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/contrib/auto-render.min.js
  13. // @require https://cdn.jsdelivr.net/npm/timeago@1.6.7/jquery.timeago.min.js
  14. // @namespace https://gitlab.com/w0mbat/user-scripts
  15. // @author w0mbat
  16. // ==/UserScript==
  17.  
  18. (async function () {
  19. 'use strict';
  20. console.log(`🐻 "View All Editorials" initializing... 🐻`)
  21.  
  22. // Utils
  23. const appendHeadChild = (tagName, options) =>
  24. Object.assign(document.head.appendChild(document.createElement(tagName)), options);
  25. const addScript = (src) => new Promise((resolve) => {
  26. appendHeadChild('script', { src, type: 'text/javascript', onload: resolve });
  27. });
  28. const addStyleSheet = (src) => new Promise((resolve) => {
  29. appendHeadChild('link', { rel: 'stylesheet', href: src, onload: resolve });
  30. });
  31. const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  32.  
  33. // KaTeX
  34. const loadKaTeX = async () =>
  35. await addStyleSheet("https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.css");
  36. const kaTexOptions = {
  37. delimiters: [
  38. { left: "$$", right: "$$", display: true },
  39. { left: "$", right: "$", display: false },
  40. { left: "\\(", right: "\\)", display: false },
  41. { left: "\\[", right: "\\]", display: true }
  42. ],
  43. ignoredTags: ["script", "noscript", "style", "textarea", "code", "option"],
  44. ignoredClasses: ["prettyprint", "source-code-for-copy"],
  45. throwOnError: false
  46. };
  47. const renderKaTeX = (rootDom) => {
  48. /* global renderMathInElement */
  49. renderMathInElement && renderMathInElement(rootDom, kaTexOptions);
  50. };
  51.  
  52. // code-prettify
  53. const loadPrettifier = async () => {
  54. await addScript("https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js?autorun=false");
  55. };
  56. /* global PR */
  57. const runPrettifier = () => PR.prettyPrint();
  58.  
  59. // jQuery TimeAgo
  60. const loadTimeAgo = async () => {
  61. /* global LANG */
  62. if (LANG == 'ja') await addScript("https://cdn.jsdelivr.net/npm/timeago@1.6.7/locales/jquery.timeago.ja.min.js");
  63. };
  64. const renderTimeAgo = () => {
  65. /* global $ */
  66. $("time.timeago").timeago();
  67. $('.tooltip-unix').each(function () {
  68. var unix = parseInt($(this).attr('title'), 10);
  69. if (1400000000 <= unix && unix <= 5000000000) {
  70. var date = new Date(unix * 1000);
  71. $(this).attr('title', date.toLocaleString());
  72. }
  73. });
  74. $('[data-toggle="tooltip"]').tooltip();
  75. };
  76.  
  77. // Editorials Loader
  78. const editorialBodyQuery = "#main-container > div.row > div:nth-child(2)";
  79. const scrape = (doc) => [
  80. doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(1)`),
  81. doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(2)`),
  82. ];
  83. const fetchEditorial = async (link) => {
  84. const response = await fetch(link.href);
  85. if (!response.ok) throw "Fetch failed";
  86. const [content, history] = scrape(new DOMParser().parseFromString(await response.text(), 'text/html'));
  87. if (!content) throw "Scraping failed";
  88. return [content, history];
  89. };
  90. const renderEditorial = (link, content, history) => {
  91. const div = link.parentNode.appendChild(document.createElement('div'));
  92. div.classList.add('🐻-editorial-content');
  93. div.appendChild(content);
  94. if (history) div.appendChild(history);
  95. renderKaTeX(div);
  96. renderTimeAgo();
  97. runPrettifier();
  98. };
  99. const loadEditorial = async (link) => {
  100. const [content, history] = await fetchEditorial(link);
  101. renderEditorial(link, content, history);
  102. };
  103.  
  104. // Lazy Loading
  105. const Timer = (callback, interval) => {
  106. let id = undefined;
  107. return {
  108. start: () => {
  109. if (id) return;
  110. callback();
  111. id = setInterval(callback, interval);
  112. },
  113. stop: () => {
  114. if (!id) return;
  115. clearInterval(id);
  116. id = undefined;
  117. },
  118. };
  119. };
  120. const Queue = (task, interval) => {
  121. const set = new Set();
  122. let timer = Timer(() => {
  123. for (const element of set) {
  124. task(element);
  125. set.delete(element);
  126. break;
  127. }
  128. if (set.size == 0) timer.stop();
  129. }, interval);
  130. return {
  131. add: (element) => {
  132. set.add(element);
  133. timer.start();
  134. },
  135. remove: (element) => set.delete(element),
  136. };
  137. };
  138. let unobserveEditorialLink = undefined;
  139. const queue = Queue(async (link) => {
  140. await loadEditorial(link)
  141. .catch(ex => console.warn(`🐻 Something wrong: "${link.href}", ${ex}`));
  142. unobserveEditorialLink(link);
  143. }, 200);
  144. const intersectionCallback = async (entries) => {
  145. for (const entry of entries) {
  146. if (entry.isIntersecting) queue.add(entry.target);
  147. else queue.remove(entry.target);
  148. }
  149. };
  150. const observeEditorialLinks = (links) => {
  151. const observer = new IntersectionObserver(intersectionCallback);
  152. unobserveEditorialLink = (link) => observer.unobserve(link);
  153. links.forEach(e => observer.observe(e));
  154. };
  155.  
  156. // initialize
  157. const init = async () => {
  158. GM_addStyle(`
  159. pre code { tab-size: 4; }
  160. ${editorialBodyQuery} > ul > li { font-size: larger; }
  161. .🐻-editorial-content { margin-top: 0.3em; font-size: smaller; }
  162. `);
  163. await loadKaTeX();
  164. await loadPrettifier();
  165. await loadTimeAgo();
  166. };
  167.  
  168. // main
  169. await init();
  170. const internalEditorialLink = (link) => link.href.match(/\/contests\/.+\/editorial\//);
  171. const notSpoiler = (link) => !link.classList.contains('spoiler');
  172. const links = [...document.getElementsByTagName('a')].filter(internalEditorialLink).filter(notSpoiler);
  173. if (links.length > 0) observeEditorialLinks(links);
  174.  
  175. console.log(`🐻 "View All Editorials" initialized. 🐻`)
  176. })();