AtCoder HashTag Setter2

ツイートボタンの埋め込みテキストに情報を追加します

当前为 2022-02-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AtCoder HashTag Setter2
  3. // @namespace https://github.com/hotarunx
  4. // @homepage https://github.com/hotarunx/atcoder-hashtag-setter2
  5. // @supportURL https://github.com/hotarunx/atcoder-hashtag-setter2/issues
  6. // @version 1.0.0
  7. // @description ツイートボタンの埋め込みテキストに情報を追加します
  8. // @author hotarunx
  9. // @match https://atcoder.jp/contests/*
  10. // @exclude https://atcoder.jp/contests/
  11. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14. "use strict";
  15. // 設定*************************************************************************
  16. /**
  17. * @type {boolean} ネタバレ防止機能
  18. * コンテストが終了前かつ常設でないコンテストのとき
  19. * ツイートボタンのテキストに問題名、ジャッジ結果、得点を含めない
  20. * default: true
  21. */
  22. const disableSpoiler = true;
  23. /** @type {string[]} 常設コンテストID一覧 ネタバレ防止機能で使う */
  24. const permanentContestIDs = [
  25. "practice",
  26. "APG4b",
  27. "abs",
  28. "practice2",
  29. "typical90",
  30. "math-and-algorithm",
  31. ];
  32. // *****************************************************************************
  33. /** ページタイプ型のリテラル 問題ページ、順位表ページなどを意味する */
  34. const pageTypes = [
  35. "tasks",
  36. "task",
  37. "clarifications",
  38. "submit",
  39. "submissions",
  40. "submission",
  41. "score",
  42. "standings",
  43. "custom_test",
  44. "editorial",
  45. undefined,
  46. ];
  47. /** ページタイプ型の型ガード */
  48. function isPageType(name) {
  49. return pageTypes.some((value) => value == name);
  50. }
  51. /**
  52. * ページからページ情報をパースして返す
  53. * @returns ページ情報
  54. */
  55. function getInfo() {
  56. /** コンテスト名 例: AtCoder Beginner Contest 210 */
  57. const contestTitle = document.getElementsByClassName("contest-title")[0]?.textContent ?? "";
  58. /**
  59. * ページのURL \
  60. * 例 (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
  61. */
  62. const url = parseURL(location.href);
  63. /** コンテストID 例: abc210 */
  64. const contestId = url[4];
  65. /**ページタイプ 例: tasks, submissions, standings, ... */
  66. const pageType = (() => {
  67. if (url.length < 6)
  68. return undefined;
  69. if (!isPageType(url[5]))
  70. return undefined;
  71. if (url.length >= 7 && url[5] === "submissions" && url[6] !== "me")
  72. return "submission";
  73. if (url.length >= 7 && url[5] === "tasks")
  74. return "task";
  75. return url[5];
  76. })();
  77. /**
  78. * 問題ID 例: abc210_a \
  79. * 問題名 A - Cabbages
  80. */
  81. const { taskId, taskTitle } = (() => {
  82. // urlの長さが7未満のとき 下記の問題ID、問題名が無いページ
  83. if (url.length < 7)
  84. return { taskId: undefined, taskTitle: undefined };
  85. if (pageType === "task") {
  86. // 問題ページのとき
  87. // URLに含まれる問題ID、問題名を返す
  88. const taskTitle = document
  89. .getElementsByClassName("h2")[0]
  90. ?.textContent?.trim()
  91. .replace(/\n.*/i, "");
  92. return { taskId: url[6], taskTitle: taskTitle };
  93. }
  94. else if (pageType === "submission") {
  95. // 提出詳細ページのとき
  96. // テーブル要素集合
  97. const tdTags = document.getElementsByTagName("td");
  98. const tdTagsArray = Array.prototype.slice.call(tdTags);
  99. // 問題の表セル要素(前の要素のテキストが`問題`の要素)を探す
  100. const taskCell = tdTagsArray.filter((elem) => {
  101. const prevElem = elem.previousElementSibling;
  102. const text = prevElem?.textContent;
  103. if (typeof text === "string")
  104. return ["問題", "Task"].includes(text);
  105. return false;
  106. })[0];
  107. if (!taskCell)
  108. return { taskId: undefined, taskTitle: undefined };
  109. const taskLink = taskCell.getElementsByTagName("a")[0];
  110. if (!taskLink)
  111. return { taskId: undefined, taskTitle: undefined };
  112. // URLに含まれる問題ID、問題名を返す
  113. const taskURLParsed = parseURL(taskLink.href);
  114. return {
  115. taskId: taskURLParsed[6],
  116. taskTitle: taskLink.textContent ?? undefined,
  117. };
  118. }
  119. // それ以外のとき 問題ID、問題名が無いページ
  120. return { taskId: undefined, taskTitle: undefined };
  121. })();
  122. /** 提出ユーザー 例: machikane */
  123. const submissionsUser = (() => {
  124. if (pageType !== "submission")
  125. return undefined;
  126. // 提出詳細ページのとき
  127. // テーブル要素集合
  128. const thTags = document.getElementsByTagName("td");
  129. const thTagsArray = Array.prototype.slice.call(thTags);
  130. // ユーザーの表セル要素(前の要素のテキストが`ユーザ`の要素)を探す
  131. const userCell = thTagsArray.filter((elem) => {
  132. const prevElem = elem.previousElementSibling;
  133. const text = prevElem?.textContent;
  134. if (typeof text === "string")
  135. return ["ユーザ", "User"].includes(text);
  136. return false;
  137. })[0];
  138. if (!userCell)
  139. return undefined;
  140. return userCell?.textContent?.trim();
  141. })();
  142. /** 提出結果 例: AC */
  143. const judgeStatus = (() => {
  144. if (pageType !== "submission")
  145. return undefined;
  146. // 提出詳細ページのとき
  147. // テーブル要素集合
  148. const thTags = document.getElementsByTagName("td");
  149. const thTagsArray = Array.prototype.slice.call(thTags);
  150. // 結果の表セル要素(前の要素のテキストが`結果`の要素)を探す
  151. const statusCell = thTagsArray.filter((elem) => {
  152. const prevElem = elem.previousElementSibling;
  153. const text = prevElem?.textContent;
  154. if (typeof text === "string")
  155. return ["結果", "Status"].includes(text);
  156. return false;
  157. })[0];
  158. if (!statusCell)
  159. return undefined;
  160. return statusCell?.textContent?.trim();
  161. })();
  162. /** 得点 例: 100 */
  163. const score = (() => {
  164. if (pageType !== "submission")
  165. return undefined;
  166. // 提出詳細ページのとき
  167. // テーブル要素集合
  168. const thTags = document.getElementsByTagName("td");
  169. const thTagsArray = Array.prototype.slice.call(thTags);
  170. // 得点の表セル要素(前の要素のテキストが`得点`の要素)を探す
  171. const scoreCell = thTagsArray.filter((elem) => {
  172. const prevElem = elem.previousElementSibling;
  173. const text = prevElem?.textContent;
  174. if (typeof text === "string")
  175. return ["得点", "Score"].includes(text);
  176. return false;
  177. })[0];
  178. if (!scoreCell)
  179. return undefined;
  180. return scoreCell?.textContent?.trim();
  181. })();
  182. return {
  183. contestTitle,
  184. contestId,
  185. pageType,
  186. taskTitle,
  187. taskId,
  188. submissionsUser,
  189. judgeStatus,
  190. score,
  191. };
  192. }
  193. /**
  194. * ツイートボタンのテキストを取得する
  195. */
  196. function getTweetButtonText() {
  197. /** ツイートボタンのHTML要素 */
  198. const a2a_kit = document.getElementsByClassName("a2a_kit")[0];
  199. if (!a2a_kit)
  200. return;
  201. /** ツイートボタンのテキスト */
  202. const a2a_title = a2a_kit.getAttribute("data-a2a-title");
  203. return a2a_title;
  204. }
  205. /**
  206. * ツイートボタンのテキストを変更する
  207. */
  208. function setTweetButtonText(text) {
  209. /** ツイートボタンのHTML要素 */
  210. const a2a_kit = document.getElementsByClassName("a2a_kit")[0];
  211. if (!a2a_kit)
  212. return "";
  213. a2a_kit.setAttribute("data-a2a-title", text);
  214. // TODO: デバッグ用
  215. console.log("tweet text :>> ", getTweetButtonText());
  216. return getTweetButtonText();
  217. }
  218. // メイン処理
  219. window.addEventListener("load", function () {
  220. const info = getInfo();
  221. // TODO: デバッグ用
  222. console.log("info :>> ", info);
  223. /** コンテストハッシュタグ 例: #AtCoder_abc210_a */
  224. const contestHashtag = info.contestId ? ` #AtCoder_${info.contestId}` : "";
  225. /** 問題ハッシュタグ 例: #AtCoder_abc210_a */
  226. const taskHashtag = info.taskId ? ` #AtCoder_${info.taskId}` : "";
  227. // ツイートボタンのテキストを取得する
  228. const text = getTweetButtonText();
  229. if (!text)
  230. return;
  231. // ページに合わせてテキストを編集する
  232. let newText = "";
  233. // コンテストが終了しているまたは常設中のコンテストか判定
  234. // コンテスト終了前にコンテストの情報をツイートボタンに含めることを防ぐため
  235. if (isContestOverOrPermanent(info.contestId ?? "") || !disableSpoiler) {
  236. // コンテストが終了しているまたは常設中のコンテスト
  237. if (info.pageType === "task") {
  238. // 個別の問題ページ
  239. // 例: A - Cabbages - AtCoder Beginner Contest 210 #AtCoder_abc210_a #AtCoder_abc210
  240. newText = text + " - " + info.contestTitle + taskHashtag + contestHashtag;
  241. }
  242. else if (info.pageType === "submission") {
  243. // 提出詳細ページ
  244. // 例: machikaneさんのA - Cabbagesへの提出 #24282585
  245. // 結果:AC
  246. // 得点:100
  247. // AtCoder Beginner Contest 210 #AtCoder_abc210_a #AtCoder_abc210
  248. // @ts-ignore
  249. // eslint-disable-next-line no-undef
  250. if (LANG === "ja") {
  251. // 日本語
  252. newText =
  253. `${info.submissionsUser}さんの${info.taskTitle}への` +
  254. text.replace(" - " + info.contestTitle, `\n結果:${info.judgeStatus}\n得点:${info.score}\n${info.contestTitle}`) +
  255. taskHashtag +
  256. contestHashtag;
  257. }
  258. else {
  259. // 英語
  260. newText =
  261. `${info.submissionsUser}'s ` +
  262. text.replace(" - " + info.contestTitle, ` to ${info.taskTitle}\nStatus: ${info.judgeStatus}\nScore: ${info.score}\n${info.contestTitle}`) +
  263. taskHashtag +
  264. contestHashtag;
  265. }
  266. }
  267. else {
  268. // その他のページ
  269. // 例: 順位表 - AtCoder Beginner Contest 210 #AtCoder_abc210
  270. newText = text + contestHashtag;
  271. }
  272. }
  273. else {
  274. // コンテストが終了していないかつ常設ではない
  275. // コンテストハッシュタグを追加するだけにする
  276. // その他のページ
  277. // 例: 順位表 - AtCoder Beginner Contest 210 #AtCoder_abc210
  278. newText = text + contestHashtag;
  279. }
  280. setTweetButtonText(newText);
  281. });
  282. /**
  283. * URLをパースする \
  284. * パラメータを消す \
  285. * 例 \
  286. * in: https://atcoder.jp/contests/abc210?lang=en \
  287. * out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
  288. */
  289. function parseURL(url) {
  290. // 区切り文字`/`で分割する
  291. // ?以降の文字列を削除してパラメータを削除する
  292. return url.split("/").map((x) => x.replace(/\?.*/i, ""));
  293. }
  294. /**
  295. * コンテストが終了しているかコンテストが常設コンテストであることを判定
  296. *
  297. * @param {string} contestId
  298. */
  299. function isContestOverOrPermanent(contestId) {
  300. // 常設中のコンテストか判定
  301. if (permanentContestIDs.includes(contestId)) {
  302. return true;
  303. }
  304. // 現在時間(UNIX時間 + 時差)
  305. const nowTime = Math.floor(Date.now() / 1000);
  306. // コンテスト終了時間
  307. // @ts-ignore
  308. // eslint-disable-next-line no-undef
  309. const contestEndTime = Math.floor(Date.parse(endTime._i) / 1000);
  310. // コンテスト終了後か判定
  311. return contestEndTime < nowTime;
  312. }