atcoder-hashtag-setter2

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

As of 2022-11-13. See the latest version.

  1. // ==UserScript==
  2. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  3. // @name atcoder-hashtag-setter2
  4. // @namespace https://github.com/hotarunx
  5. // @version 2.0.0
  6. // @description ツイートボタンの埋め込みテキストに情報を追加します
  7. // @author hotarunx
  8. // @license MIT
  9. // @supportURL https://github.com/hotarunx/atcoder-hashtag-setter2/issues
  10. // @match https://atcoder.jp/contests/*
  11. // @exclude https://atcoder.jp/contests/
  12. // @grant none
  13. // ==/UserScript==
  14. // AtCoderの問題ページをパースする
  15. /**
  16. * URLをパースする パラメータを消す \
  17. * 例: in: https://atcoder.jp/contests/abc210?lang=en \
  18. * 例: out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
  19. */
  20. const parseURL = (url) => {
  21. // 区切り文字`/`で分割する
  22. // ?以降の文字列を削除してパラメータを削除する
  23. return url.split("/").map((x) => x.replace(/\?.*/i, ""));
  24. };
  25. const URL = parseURL(window.location.href);
  26. //
  27. /**
  28. * 表セル要素から、前の要素のテキストが引数と一致する要素を探す
  29. * 個別の提出ページで使うことを想定
  30. * 例: searchSubmissionInfo(["問題", "Task"])
  31. */
  32. const searchSubmissionInfo = (key) => {
  33. const tdTags = document.getElementsByTagName("td");
  34. const tdTagsArray = Array.prototype.slice.call(tdTags);
  35. return tdTagsArray.filter((elem) => {
  36. const prevElem = elem.previousElementSibling;
  37. const text = prevElem?.textContent;
  38. if (typeof text === "string")
  39. return key.includes(text);
  40. return false;
  41. })[0];
  42. };
  43. /** コンテストタイトル 例: AtCoder Beginner Contest 210 */
  44. const contestTitle = document.getElementsByClassName("contest-title")[0]?.textContent ?? "";
  45. /** コンテストID 例: abc210 */
  46. const contestID = URL[4] ?? "";
  47. /**
  48. * ページ種類 \
  49. * 基本的にコンテストIDの次のパス
  50. * ### 例外
  51. * 問題: task
  52. * 個別の提出: submission
  53. */
  54. const pageType = (() => {
  55. if (URL.length < 6)
  56. return "";
  57. if (URL.length >= 7 && URL[5] === "submissions" && URL[6] !== "me")
  58. return "submission";
  59. if (URL.length >= 7 && URL[5] === "tasks")
  60. return "task";
  61. return URL[5] ?? "";
  62. })();
  63. /** 問題ID 例: abc210_a */
  64. const taskID = (() => {
  65. if (pageType === "task") {
  66. // 問題ページでは、URLから問題IDを取り出す
  67. return URL[6] ?? "";
  68. }
  69. if (pageType === "submission") {
  70. // 個別の提出ページでは、問題リンクのURLから問題IDを取り出す
  71. // 提出情報の問題のURLを取得する
  72. const taskCell = searchSubmissionInfo(["問題", "Task"]);
  73. if (!taskCell)
  74. return "";
  75. const taskLink = taskCell.getElementsByTagName("a")[0];
  76. if (!taskLink)
  77. return "";
  78. const taskUrl = parseURL(taskLink.href);
  79. const taskIDParsed = taskUrl[6] ?? "";
  80. return taskIDParsed;
  81. }
  82. return "";
  83. })();
  84. /** 問題名 例: A - Cabbages */
  85. const taskTitle = (() => {
  86. if (pageType === "task") {
  87. // 問題ページでは、h2から問題名を取り出す
  88. return (document
  89. .getElementsByClassName("h2")[0]
  90. ?.textContent?.trim()
  91. .replace(/\n.*/i, "") ?? "");
  92. }
  93. if (pageType === "submission") {
  94. // 個別の提出ページでは、問題リンクのテキストから問題名を取り出す
  95. // 提出情報の問題のテキストを取得する
  96. const taskCell = searchSubmissionInfo(["問題", "Task"]);
  97. if (!taskCell)
  98. return "";
  99. const taskLink = taskCell.getElementsByTagName("a")[0];
  100. if (!taskLink)
  101. return "";
  102. return taskLink.textContent ?? "";
  103. }
  104. return "";
  105. })();
  106. /** 提出ユーザー 例: machikane */
  107. const submissionsUser = (() => {
  108. if (pageType !== "submission")
  109. return "";
  110. // 個別の提出ページのとき
  111. const userCell = searchSubmissionInfo(["ユーザ", "User"]);
  112. if (!userCell)
  113. return "";
  114. return userCell?.textContent?.trim() ?? "";
  115. })();
  116. /** 提出結果 例: AC */
  117. const judgeStatus = (() => {
  118. if (pageType !== "submission")
  119. return "";
  120. // 個別の提出ページのとき
  121. const statusCell = searchSubmissionInfo(["結果", "Status"]);
  122. if (!statusCell)
  123. return "";
  124. return statusCell?.textContent?.trim() ?? "";
  125. })();
  126. /** 得点 例: 100 */
  127. const judgeScore = (() => {
  128. if (pageType !== "submission")
  129. return 0;
  130. // 個別の提出ページのとき
  131. const scoreCell = searchSubmissionInfo(["得点", "Score"]);
  132. if (!scoreCell)
  133. return 0;
  134. return parseInt(scoreCell?.textContent?.trim() ?? "0", 10);
  135. })();
  136.  
  137. /** 常設コンテストID一覧 */
  138. const permanentContestIDs = [
  139. "practice",
  140. "APG4b",
  141. "abs",
  142. "practice2",
  143. "typical90",
  144. "math-and-algorithm",
  145. "tessoku-book",
  146. ];
  147. /**
  148. * 次を判定
  149. * * コンテストが終了している
  150. * * コンテストが常設コンテスト
  151. */
  152. var isContestOver = () => {
  153. if (permanentContestIDs.includes(contestID))
  154. return true;
  155. return Date.now() > endTime.valueOf();
  156. };
  157.  
  158. var html = "<a target=\"_blank\" href=\"\" rel=\"nofollow noopener\">\n <span class=\"a2a_svg a2a_s__default a2a_s_twitter\" style=\"background-color: rgb(230, 18, 114);\n width: 20px;\n line-height: 20px;\n height: 20px;\n background-size: 20px;\n border-radius: 3px;\n --darkreader-inline-bgcolor: #0c72b7;\" data-darkreader-inline-bgcolor=\"\">\n <svg focusable=\"false\" aria-hidden=\"true\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">\n <path fill=\"#FFF\" d=\"M28 8.557a9.913 9.913 0 0 1-2.828.775 4.93 4.93 0 0 0 2.166-2.725 9.738 9.738 0 0 1-3.13 1.194 4.92 4.92 0 0 0-3.593-1.55 4.924 4.924 0 0 0-4.794 6.049c-4.09-.21-7.72-2.17-10.15-5.15a4.942 4.942 0 0 0-.665 2.477c0 1.71.87 3.214 2.19 4.1a4.968 4.968 0 0 1-2.23-.616v.06c0 2.39 1.7 4.38 3.952 4.83-.414.115-.85.174-1.297.174-.318 0-.626-.03-.928-.086a4.935 4.935 0 0 0 4.6 3.42 9.893 9.893 0 0 1-6.114 2.107c-.398 0-.79-.023-1.175-.068a13.953 13.953 0 0 0 7.55 2.213c9.056 0 14.01-7.507 14.01-14.013 0-.213-.005-.426-.015-.637.96-.695 1.795-1.56 2.455-2.55z\" data-darkreader-inline-fill=\"\" style=\"--darkreader-inline-fill: #e8e6e3\"></path>\n </svg> </span><span class=\"a2a_label\">Twitter2</span>\n</a>\n";
  159.  
  160. /** ツイートボタンのHTML要素 */
  161. const a2akit = document.getElementsByClassName("a2a_kit")[0];
  162. /**
  163. * ツイートボタンのテキストを取得する
  164. */
  165. const getTweetButtonText = () => {
  166. if (!a2akit)
  167. return "";
  168. return a2akit.getAttribute("data-a2a-title");
  169. };
  170. /**
  171. * ツイートボタンのテキストを変更する
  172. */
  173. const setTweetButtonText = (text) => {
  174. if (!a2akit)
  175. return;
  176. a2akit.setAttribute("data-a2a-title", text);
  177. };
  178. const addTweetSearchButton = (text) => {
  179. const searchURL = `https://twitter.com/search?q=${encodeURIComponent(text)}`;
  180. const parser = new DOMParser();
  181. const doc = parser.parseFromString(html, "text/html");
  182. const docA = doc.getElementsByTagName("a")[0];
  183. docA.href = searchURL;
  184. a2akit.insertAdjacentElement("beforeend", docA);
  185. };
  186.  
  187. (() => {
  188. // ネタバレ防止のため、コンテスト終了前(常設コンテストを除く)は無効とする
  189. if (!isContestOver())
  190. return;
  191. /** コンテストハッシュタグ 例: #AtCoder_abc210_a */
  192. const contestHashtag = contestID ? ` #AtCoder_${contestID}` : "";
  193. /** 問題ハッシュタグ 例: #AtCoder_abc210_a */
  194. const taskHashtag = taskID ? ` #AtCoder_${taskID}` : "";
  195. /** ユーザーハッシュタグ 例: #AtCoder_machikane */
  196. const userHashtag = userScreenName ? ` #AtCoder_${userScreenName}` : "";
  197. // ツイートボタンのテキストを取得する
  198. const textOrg = getTweetButtonText();
  199. if (!textOrg)
  200. return;
  201. // ツイートボタンのテキストを編集
  202. let text = textOrg + contestHashtag;
  203. if (pageType === "task") {
  204. // 問題ページ
  205. text = `${taskTitle} - ${contestTitle}${taskHashtag}${contestHashtag}${userHashtag}`;
  206. }
  207. else if (pageType === "submission") {
  208. // 個別の提出ページ
  209. text = `${taskTitle} - ${submissionsUser}の提出 - 結果 ${judgeStatus} ${judgeScore}点 - ${contestTitle}${taskHashtag}${contestHashtag}${userHashtag}`;
  210. }
  211. setTweetButtonText(text);
  212. // タグ検索ボタンを追加
  213. /** 検索タグ 問題ハッシュタグ なければコンテストハッシュタグ */
  214. const searchTag = taskHashtag || contestHashtag;
  215. addTweetSearchButton(searchTag.trim());
  216. })();