// ==UserScript==
// @name View All Editorials
// @name:ja 解説ぜんぶ見る
// @description View all editorials of the AtCoder contest in one page.
// @description:ja AtCoderコンテストの解説ページに、すべての問題の解説をまとめて表示します。
// @version 1.5.0
// @icon https://www.google.com/s2/favicons?domain=atcoder.jp
// @match https://atcoder.jp/contests/*/editorial
// @match https://atcoder.jp/contests/*/editorial?*
// @grant GM_addStyle
// @require https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.js
// @require https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/contrib/auto-render.min.js
// @require https://cdn.jsdelivr.net/npm/timeago@1.6.7/jquery.timeago.min.js
// @namespace https://gitlab.com/w0mbat/user-scripts
// @author w0mbat
// ==/UserScript==
(async function () {
'use strict';
console.log(`🐻 "View All Editorials" initializing... 🐻`)
// Utils
const appendHeadChild = (tagName, options) =>
Object.assign(document.head.appendChild(document.createElement(tagName)), options);
const addScript = (src) => new Promise((resolve) => {
appendHeadChild('script', { src, type: 'text/javascript', onload: resolve });
});
const addStyleSheet = (src) => new Promise((resolve) => {
appendHeadChild('link', { rel: 'stylesheet', href: src, onload: resolve });
});
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// KaTeX
const loadKaTeX = async () =>
await addStyleSheet("https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.css");
const kaTexOptions = {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
{ left: "\\(", right: "\\)", display: false },
{ left: "\\[", right: "\\]", display: true }
],
ignoredTags: ["script", "noscript", "style", "textarea", "code", "option"],
ignoredClasses: ["prettyprint", "source-code-for-copy"],
throwOnError: false
};
const renderKaTeX = (rootDom) => {
/* global renderMathInElement */
renderMathInElement && renderMathInElement(rootDom, kaTexOptions);
};
// code-prettify
const loadPrettifier = async () => {
await addScript("https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js?autorun=false");
};
/* global PR */
const runPrettifier = () => PR.prettyPrint();
// jQuery TimeAgo
const loadTimeAgo = async () => {
/* global LANG */
if (LANG == 'ja') await addScript("https://cdn.jsdelivr.net/npm/timeago@1.6.7/locales/jquery.timeago.ja.min.js");
};
const renderTimeAgo = () => {
/* global $ */
$("time.timeago").timeago();
$('.tooltip-unix').each(function () {
var unix = parseInt($(this).attr('title'), 10);
if (1400000000 <= unix && unix <= 5000000000) {
var date = new Date(unix * 1000);
$(this).attr('title', date.toLocaleString());
}
});
$('[data-toggle="tooltip"]').tooltip();
};
// Editorials Loader
const editorialBodyQuery = "#main-container > div.row > div:nth-child(2)";
const scrape = (doc) => [
doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(1)`),
doc.querySelector(`${editorialBodyQuery} > div:nth-of-type(2)`),
];
const fetchEditorial = async (link) => {
const response = await fetch(link.href);
if (!response.ok) throw "Fetch failed";
const [content, history] = scrape(new DOMParser().parseFromString(await response.text(), 'text/html'));
if (!content) throw "Scraping failed";
return [content, history];
};
const renderEditorial = (link, content, history) => {
const div = link.parentNode.appendChild(document.createElement('div'));
div.classList.add('🐻-editorial-content');
div.appendChild(content);
if (history) div.appendChild(history);
renderKaTeX(div);
renderTimeAgo();
runPrettifier();
};
const loadEditorial = async (link) => {
const [content, history] = await fetchEditorial(link);
renderEditorial(link, content, history);
};
// Lazy Loading
const Timer = (callback, interval) => {
let id = undefined;
return {
start: () => {
if (id) return;
callback();
id = setInterval(callback, interval);
},
stop: () => {
if (!id) return;
clearInterval(id);
id = undefined;
},
};
};
const Queue = (task, interval) => {
const set = new Set();
let timer = Timer(() => {
for (const element of set) {
task(element);
set.delete(element);
break;
}
if (set.size == 0) timer.stop();
}, interval);
return {
add: (element) => {
set.add(element);
timer.start();
},
remove: (element) => set.delete(element),
};
};
let unobserveEditorialLink = undefined;
const queue = Queue(async (link) => {
await loadEditorial(link)
.catch(ex => console.warn(`🐻 Something wrong: "${link.href}", ${ex}`));
unobserveEditorialLink(link);
}, 200);
const intersectionCallback = async (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) queue.add(entry.target);
else queue.remove(entry.target);
}
};
const observeEditorialLinks = (links) => {
const observer = new IntersectionObserver(intersectionCallback);
unobserveEditorialLink = (link) => observer.unobserve(link);
links.forEach(e => observer.observe(e));
};
// initialize
const init = async () => {
GM_addStyle(`
pre code { tab-size: 4; }
${editorialBodyQuery} > ul > li { font-size: larger; }
.🐻-editorial-content { margin-top: 0.3em; font-size: smaller; }
`);
await loadKaTeX();
await loadPrettifier();
await loadTimeAgo();
};
// main
await init();
const internalEditorialLink = (link) => link.href.match(/\/contests\/.+\/editorial\//);
const notSpoiler = (link) => !link.classList.contains('spoiler');
const links = [...document.getElementsByTagName('a')].filter(internalEditorialLink).filter(notSpoiler);
if (links.length > 0) observeEditorialLinks(links);
console.log(`🐻 "View All Editorials" initialized. 🐻`)
})();