View All Editorials

View all editorials of the AtCoder contest in one page.

当前为 2022-10-16 提交的版本,查看 最新版本

  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.4.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://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.6.7/jquery.timeago.min.js
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.6.7/locales/jquery.timeago.ja.min.js
  15. // @namespace https://gitlab.com/w0mbat/user-scripts
  16. // @author w0mbat
  17. // ==/UserScript==
  18.  
  19. (async function () {
  20. 'use strict';
  21. console.log(`🐻 "View All Editorials" start execution. 🐻`)
  22.  
  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.  
  32. const loadKaTeX = async () =>
  33. await addStyleSheet("https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.css");
  34. const kaTexOptions = {
  35. delimiters: [
  36. { left: "$$", right: "$$", display: true },
  37. { left: "$", right: "$", display: false },
  38. { left: "\\(", right: "\\)", display: false },
  39. { left: "\\[", right: "\\]", display: true }
  40. ],
  41. ignoredTags: ["script", "noscript", "style", "textarea", "code", "option"],
  42. ignoredClasses: ["prettyprint", "source-code-for-copy"],
  43. throwOnError: false
  44. };
  45. const renderKaTeX = (rootDom) => {
  46. /* global renderMathInElement */
  47. renderMathInElement && renderMathInElement(rootDom, kaTexOptions);
  48. };
  49.  
  50. const loadPrettifier = async () =>
  51. await addScript("https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?autorun=false");
  52. /* global PR */
  53. const runPrettifier = () => PR.prettyPrint();
  54.  
  55. const renderTimeAgo = () => {
  56. /* global $ */
  57. $("time.timeago").timeago();
  58. $('.tooltip-unix').each(function () {
  59. var unix = parseInt($(this).attr('title'), 10);
  60. if (1400000000 <= unix && unix <= 5000000000) {
  61. var date = new Date(unix * 1000);
  62. $(this).attr('title', date.toLocaleString());
  63. }
  64. });
  65. $('[data-toggle="tooltip"]').tooltip();
  66. };
  67.  
  68. const editorialBodyQuery = "#main-container > div.row > div:nth-child(2)";
  69. const scrape = (doc) => [
  70. doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(1)`),
  71. doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(2)`),
  72. ];
  73. const loadEditorial = async (link) => {
  74. const response = await fetch(link.href);
  75. if (!response.ok) throw "Fetch failed";
  76. const [content, history] = scrape(new DOMParser().parseFromString(await response.text(), 'text/html'));
  77. if (!content || !history) throw "Scraping failed";
  78. const div = link.parentNode.appendChild(document.createElement('div'));
  79. div.classList.add('🐻-editorial-content');
  80. div.appendChild(content);
  81. div.appendChild(history);
  82. renderKaTeX(div);
  83. renderTimeAgo();
  84. runPrettifier();
  85. };
  86.  
  87. const intersectionCallback = (entries, observer) => {
  88. entries.forEach(async entry => {
  89. if (entry.isIntersecting) {
  90. await loadEditorial(entry.target)
  91. .catch(ex => console.warn(`🐻 Something wrong: "${entry.target.href}", ${ex}`));
  92. observer.unobserve(entry.target);
  93. }
  94. });
  95. };
  96. const observeEditorialLinks = (links) => {
  97. const observer = new IntersectionObserver(intersectionCallback);
  98. links.forEach(e => observer.observe(e));
  99. };
  100.  
  101. GM_addStyle(`
  102. pre code { tab-size: 2; }
  103. ${editorialBodyQuery} > ul > li { font-size: larger; }
  104. .🐻-editorial-content { margin-top: 0.3em; font-size: smaller; }
  105. `);
  106. await loadKaTeX();
  107. await loadPrettifier();
  108.  
  109. const filter4InternalEditorialLink = (link) => link.href.match(/\/contests\/.+\/editorial\//);
  110. const links = Array.prototype.filter.call(document.getElementsByTagName('a'), filter4InternalEditorialLink);
  111. if (links.length > 0) await loadEditorial(links[0]);
  112. if (links.length > 1) observeEditorialLinks(links.slice(1));
  113.  
  114. console.log(`🐻 "View All Editorials" end execution. 🐻`)
  115. })();