您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。
// ==UserScript== // @name AtCoder Editorial for Typical90 // @namespace https://github.com/KATO-Hiro // @version 0.6.0 // @description AtCoder「競プロ典型 90 問」に解説タブを追加し、E869120さんがGitHubで公開されている問題の解説・想定ソースコードなどのリンクを表示します。 // @match https://atcoder.jp/contests/typical90* // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.5/dayjs.min.js // @author hiro_hiro // @license CC0 // @downloadURL // @updateURL // @homepage https://github.com/KATO-Hiro/AtCoder-Editorial-for-Typical90 // @supportURL https://github.com/KATO-Hiro/AtCoder-Editorial-for-Typical90/issues // @grant GM_addStyle // ==/UserScript== (async function () { "use strict"; addTabs(); const tasks = await fetchTasks(); // TODO: Use cache to reduce access to AtCoder. addEditorialPage(tasks); $(".nav-tabs a").click(function () { changeTab($(this)); hideContentsOfPreviousPage(); return false; }); // TODO: 「解説」ボタンをクリックしたら、該当する問題のリンクを表示できるようにする })(); function addTabs() { addTabContentStyles(); addTabContents(); addEditorialTab(); } function addTabContentStyles() { const tabContentStyles = ` .tab-content { display: none; } .tab-content.active { display: block; } `; GM_addStyle(tabContentStyles); } function addTabContents() { const contestNavTabsId = document.getElementById("contest-nav-tabs"); // See: // https://stackoverflow.com/questions/268490/jquery-document-createelement-equivalent // https://blog.toshimaru.net/jqueryhidden-inputjquery/ const idNames = [ "editorial-created-by-userscript" ]; for (const idName of idNames) { $("<div>", { class: "tab-content", id: idName, }).appendTo(contestNavTabsId); } } // FIXME: Hard coding is not good. function addEditorialTab() { // See: // https://api.jquery.com/before/ $("li.pull-right").before("<li><a href='#editorial-created-by-userscript'><span class='glyphicon glyphicon-book' style='margin-right:4px;' aria-hidden='true'></span>解説</a></li>"); } function padZero(taskId) { // See: // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/padStart return String(taskId).padStart(3, '0'); } // TODO: キャッシュを利用して、本家へのアクセスを少なくなるようにする async function fetchTasks() { const tbodies = await fetchTaskPage(); const tasks = new Object(); let taskCount = 1; for (const [index, aTag] of Object.entries($(tbodies).find("a"))) { // Ignore a-tags including task-id and "Submit". // See: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes if (index % 3 == 1 && aTag.text.includes("★")) { const taskId = String(taskCount).padStart(3, "0"); tasks[taskId] = [aTag.text, aTag.href]; taskCount += 1; } } return tasks; } async function fetchTaskPage() { // See: // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch // https://developer.mozilla.org/en-US/docs/Web/API/Body/text // https://developer.mozilla.org/ja/docs/Web/API/DOMParser // https://api.jquery.com/each/ // http://dyn-web.com/tutorials/object-literal/properties.php#:~:text=Add%20a%20Property%20to%20an%20Existing%20Object%20Literal&text=myObject.,if%20it%20is%20a%20string). const tbodies = await fetch("https://atcoder.jp/contests/typical90/tasks", { method: "GET" }) .then(response => { return response.text() }) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const messages = doc.querySelector("#main-container > div.row > div:nth-child(2) > div > table > tbody"); return messages; }) .catch(error => { console.warn('Something went wrong.', error); }); return tbodies; } function addEditorialPage(tasks) { const editorialId = "#editorial-created-by-userscript"; showHeader("editorial-header", "解説", editorialId); addHorizontalRule(editorialId); showDifficultyVotingAndUserCodes(editorialId); let taskEditorialsDiv = addDiv("task-editorials", editorialId); taskEditorialsDiv = "." + taskEditorialsDiv; addEditorials(tasks, taskEditorialsDiv); } function showHeader(className, text, tag) { addHeader( "<h2>", // heading_tag className, // className text, // text tag // parent_tag ); } function addHeader(heading_tag, className, text, parent_tag) { $(heading_tag, { class: className, text: text, }).appendTo(parent_tag); } function addHorizontalRule(tag) { // See: // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr $("<hr>", { class: "", }).appendTo(tag); } function showDifficultyVotingAndUserCodes(tag) { addHeader( "<h3>", // heading_tag "difficulty-voting-and-user-codes", // className "問題の難易度を投票する・ソースコードを共有する", // text tag // parent_tag ); $("<ul>", { class: "spread-sheets-ul", text: "" }).appendTo(tag); const spreadSheetUrl = "https://docs.google.com/spreadsheets/d/1GG4Higis4n4GJBViVltjcbuNfyr31PzUY_ZY1zh2GuI/edit#gid="; const homeID = "0"; addSpreadSheetHomeURL(spreadSheetUrl + homeID); const difficultyVotingID = "1593175261"; addDifficultyVotingURL(spreadSheetUrl + difficultyVotingID); const taskGroups = [ ["001", "023", spreadSheetUrl + "105162261"], // task start, task end, spread sheet id. ["024", "047", spreadSheetUrl + "1671161250"], ["048", "071", spreadSheetUrl + "671876031"], ["072", "090", spreadSheetUrl + "428850451"] ]; // See: // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach taskGroups.forEach( taskGroup => { const taskStart = taskGroup[0]; const taskEnd = taskGroup[1]; const url = taskGroup[2]; addUserCodesURL( taskStart, taskEnd, url ); } ); } function addSpreadSheetHomeURL(url) { $("<li>", { class: "spread-sheet-home-li", text: "" }).appendTo(".spread-sheets-ul"); $("<a>", { class: "spread-sheet-home-url", href: url, text: "目的", target: "_blank", rel: "noopener", }).appendTo(".spread-sheet-home-li"); } function addDifficultyVotingURL(url) { $("<li>", { class: "difficulty-voting-li", text: "" }).appendTo(".spread-sheets-ul"); $("<a>", { class: "difficulty-voting-url", href: url, text: "問題の難易度を投票する", target: "_blank", rel: "noopener", }).appendTo(".difficulty-voting-li"); } function addUserCodesURL(taskStart, taskEnd, url) { // See: // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals $("<li>", { class: `user-codes-${taskStart}-${taskEnd}-li`, text: "" }).appendTo(".spread-sheets-ul"); $("<a>", { class: `user-codes-${taskStart}-${taskEnd}-url`, href: url, text: `ソースコード(${taskStart}〜${taskEnd})を見る・共有する`, target: "_blank", rel: "noopener", }).appendTo(`.user-codes-${taskStart}-${taskEnd}-li`); } function addDiv(tagName, parentTag) { $("<div>", { class: tagName, }).appendTo(parentTag); return tagName; } function addEditorials(tasks, parentTag) { const githubRepoUrl = getGitHubRepoUrl(); const editorialsUrl = githubRepoUrl + "editorial/"; const codesUrl = githubRepoUrl + "sol/"; // See: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice const latestTaskId = Object.keys(tasks).slice(-1)[0]; // HACK: 公開当日分の問題についてはリンク切れを回避するため、解説・ソースコードの一覧を示すことで応急的に対処 // HACK: 問題によっては、複数の解説とソースコードが公開される日もある // getMultipleEditorialUrlsIfNeeds()とgetMultipleCodeUrls()で、アドホック的に対処している for (const [taskId, [taskName, taskUrl]] of Object.entries(tasks)) { let taskEditorialDiv = addDiv(`task-${taskId}-editorial`, parentTag); taskEditorialDiv = "." + taskEditorialDiv; // See: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split showTaskName(taskId, `${taskId} - ${taskName}`, taskUrl, taskEditorialDiv); const additionalUrls = getMultipleEditorialUrlsIfNeeds(taskId); // TODO: AtCoderの解説ページで図を表示できるようにする for (const [index, additionalUrl] of Object.entries(additionalUrls)) { const editorialUrl = editorialsUrl + taskId + additionalUrl + ".jpg"; showEditorial(taskId + additionalUrl, editorialUrl, additionalUrl, taskEditorialDiv); } const codeUrls = getMultipleCodeUrls(taskId); // TODO: ソースコードをフォーマットされた状態で表示する for (const [index, codeUrl] of Object.entries(codeUrls)) { const editorialCodelUrl = codesUrl + taskId + codeUrl; const [additionalUrl, language] = codeUrl.split("."); showCode(taskId + additionalUrl, editorialCodelUrl, codeUrl, taskEditorialDiv); } } } function getGitHubRepoUrl() { const url = "https://github.com/E869120/kyopro_educational_90/blob/main/"; return url; } function showTaskName(taskId, taskName, taskUrl, tag) { const taskIdClass = `task-${taskId}`; addHeader( "<h3>", // heading_tag taskIdClass, // className taskName, // text tag // parent_tag ); $("<a>", { class: `${`task-${taskId}-url`} small glyphicon glyphicon-new-window`, href: taskUrl, target: "_blank", }).appendTo(`.${taskIdClass}`); } // TODO: 複数の解説資料がアップロードされた日があれば更新する function getMultipleEditorialUrlsIfNeeds(taskId) { // See: // https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Property_Accessors // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in // タスク名: 解説ファイルの番号 // 0xx-yyy.jpgの0xxをキーに、-yyyを値としている const multipleEditorialUrls = { "005": ["-01", "-02", "-03"], "011": ["-01", "-02"], "017": ["-01", "-02", "-03"], "023": ["-01", "-02", "-03", "-04"], "029": ["-01", "-02"], "035": ["-01", "-02", "-03"], "041": ["-01", "-02", "-03"], "047": ["-01", "-02"], "053": ["-01", "-02", "-03", "-04"], "059": ["-01", "-02", "-03"], "065": ["-01", "-02", "-03"], "071": ["-01", "-02", "-03"], "077": ["-01", "-02", "-03"], "083": ["-01", "-02", "-03", "-04"], "084": ["-01", "-02"], "085": ["-01", "-02"], "086": ["-01", "-02"], "087": ["-01", "-02"], "088": ["-01", "-02"], "089": ["-01", "-02", "-03", "-04"], "090": ["-01", "-02", "-03", "-04", "-05", "-06"], }; if (taskId in multipleEditorialUrls) { return multipleEditorialUrls[taskId]; } else { return [""]; // dummy } } // TODO: 複数の想定コードがアップロードされた日があれば更新する function getMultipleCodeUrls(taskId) { // タスク名: ソースコードの番号と拡張子 // 0xx-yyy.langの0xxをキーに、-yyy.langを値としている const multipleCodeUrls = { "005": ["-01.cpp", "-02.cpp", "-03.cpp"], "011": ["-01.cpp", "-02.cpp", "-03.cpp"], "017": ["-01.cpp", "-02.cpp", "-03.cpp"], "023": ["-01.cpp", "-02.cpp", "-03.cpp", "-04a.cpp", "-04b.cpp"], "029": ["-01.cpp", "-02.cpp", "-03.cpp"], "035": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"], "041": ["-01a.cpp", "-01b.cpp", "-02.cpp", "-03.cpp"], "047": ["-01.cpp", "-02.cpp"], "053": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp"], "055": [".cpp", "-02.py", "-03.py"], "059": ["-01.cpp", "-02.cpp"], "061": ["-01.cpp", "-02.cpp"], "065": ["-01.cpp", "-02.cpp", "-03.cpp"], "066": ["a.cpp", "b.cpp"], "068": ["a.cpp", "b.cpp"], "071": ["-02.cpp", "-03.cpp"], "077": ["-01.cpp", "-02.cpp", "-03.cpp", "-04a.cpp", "-04b.cpp"], "080": ["a.cpp", "b.cpp"], "082": ["a.cpp", "b.cpp"], "083": ["-01.cpp", "-02a.cpp", "-02b.cpp"], "084": ["-01.cpp", "-02.cpp"], "089": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp", "-05.cpp"], "090": ["-01.cpp", "-02.cpp", "-03.cpp", "-04.cpp", "-05.cpp", "-06a.cpp", "-06b.cpp", "-07a.cpp", "-07b.cpp"], }; if (taskId in multipleCodeUrls) { return multipleCodeUrls[taskId]; } else { return [".cpp"]; } } function addNote(className, message, parent_tag) { $("<p>", { class: className, text: message, }).appendTo(parent_tag); } function showEditorial(taskId, url, additionalUrl, tag) { const ulClass = `editorial-${taskId}-ul`; const liClass = `editorial-${taskId}-li`; $("<ul>", { class: ulClass, text: "" }).appendTo(tag); $("<li>", { class: liClass, text: "" }).appendTo(`.${ulClass}`); $("<a>", { class: `editorial-${taskId}-url`, href: url, text: `公式解説${additionalUrl}`, target: "_blank", rel: "noopener", }).appendTo(`.${liClass}`); } function showCode(taskId, url, additionalUrl, tag) { const ulClass = `editorial-${taskId}-code-ul`; const liClass = `editorial-${taskId}-code-li`; $("<ul>", { class: ulClass, text: "" }).appendTo(tag); $("<li>", { class: liClass, text: "" }).appendTo(`.${ulClass}`); $("<a>", { class: `editorial-${taskId}-code-url`, href: url, text: `想定ソースコード${additionalUrl}`, target: "_blank", rel: "noopener", }).appendTo(`.${liClass}`); } function addEditorialButtonToTaskPage() { // See: // https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement const editorialButton = document.createElement("a"); editorialButton.classList.add("btn", "btn-default", "btn-sm"); editorialButton.textContent = "解説"; const taskTitle = document.querySelector(".row > div > .h2"); if (taskTitle) { taskTitle.appendChild(editorialButton); return editorialButton; } else { return; } } function changeTab(this_object) { // See: // https://api.jquery.com/parent/ // https://api.jquery.com/addClass/#addClass-className // https://api.jquery.com/siblings/#siblings-selector // https://api.jquery.com/removeClass/#removeClass-className // https://www.design-memo.com/coding/jquery-tab-change this_object.parent().addClass("active").siblings(".active").removeClass("active"); const tabContentsUrl = this_object.attr("href"); $(tabContentsUrl).addClass("active").siblings(".active").removeClass("active"); } function hideContentsOfPreviousPage() { // See: // https://api.jquery.com/length/ // https://api.jquery.com/hide/ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String const tagCount = $(".col-sm-12").length; for (let index = 0; index < tagCount; index++) { if (index != 0) { $("#main-container > div.row > div:nth-child(" + String(index + 1) + ")").hide(); } } }