atcoder-standings-lang

AtCoder の順位表に最多提出言語を追加します.uesugi6111 さん作のスクリプトが元ネタです.

当前为 2020-11-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-standings-lang
  3. // @namespace iilj
  4. // @version 2020.11.10.0
  5. // @description AtCoder の順位表に最多提出言語を追加します.uesugi6111 さん作のスクリプトが元ネタです.
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/atcoder-standings-lang/issues
  8. // @match https://atcoder.jp/contests/*/standings*
  9. // ==/UserScript==
  10.  
  11. /* globals $ */
  12.  
  13. /**
  14. * ユーザID/言語ごとの提出数
  15. * @typedef {Object} UserLangEntry
  16. * @property {string} user_id ユーザ ID
  17. * @property {string} language 言語名
  18. * @property {number} count 提出数
  19. */
  20.  
  21. (() => {
  22. 'use strict';
  23.  
  24. /** @type {Map<string, UserLangEntry[]>} */
  25. const userLangEntryMap = new Map();
  26.  
  27. const fetchLangJson = async () => {
  28. console.log('@kenkooooさんありがとう');
  29. // 2e5 個くらい要素があるのでキャッシュする
  30. const res = await fetch("https://kenkoooo.com/atcoder/resources/lang.json", { cache: 'force-cache' });
  31. /** @type {UserLangEntry[]} */
  32. const userLangEntries = await res.json();
  33.  
  34. // prepare map
  35. userLangEntries.forEach(userLangEntry => {
  36. if (userLangEntryMap.has(userLangEntry.user_id)) {
  37. userLangEntryMap.get(userLangEntry.user_id).push(userLangEntry);
  38. } else {
  39. userLangEntryMap.set(userLangEntry.user_id, [userLangEntry]);
  40. }
  41. });
  42.  
  43. // sort arrays
  44. userLangEntryMap.forEach(userLangArray => {
  45. userLangArray.sort((a, b) => b.count - a.count); // in place
  46. });
  47. };
  48.  
  49. /** @type {(anchor: HTMLAnchorElement) => void} */
  50. const updateAnchor = (anchor) => {
  51. if (!anchor.href.includes('/users/')) return;
  52.  
  53. const user_id = anchor.text.trim();
  54. if (!userLangEntryMap.has(user_id)) return;
  55.  
  56. const userLangArray = userLangEntryMap.get(user_id);
  57.  
  58. const tooltipHtml = userLangArray.map((userLangEntry) =>
  59. `${userLangEntry.language} : ${userLangEntry.count}`
  60. ).join('<br>');
  61.  
  62. let langHtml = userLangArray[0].language;
  63. if (userLangArray.length >= 2) {
  64. langHtml += '<div style="font-size:10px;display:inline;">'
  65. + `/${userLangArray[1].language}`
  66. + (userLangArray.length >= 3 ? `/${userLangArray[2].language}` : '')
  67. + ' </div>';
  68. }
  69.  
  70. anchor.insertAdjacentHTML('beforeend',
  71. '/'
  72. + '<div data-toggle="tooltip" data-html="true" data-placement="right" style="font-size:12px;display:inline;" title="' + tooltipHtml + '">'
  73. + langHtml
  74. + '</div>');
  75. $('[data-toggle="tooltip"]').tooltip();
  76. };
  77.  
  78. /** @type {(tbody: HTMLTableSectionElement) => void} */
  79. const updateTable = (tbody) => {
  80. tbody.querySelectorAll('.username').forEach(anchor => {
  81. updateAnchor(anchor);
  82. });
  83. };
  84.  
  85. /** @type {HTMLTableSectionElement} */
  86. let tbody = null;
  87.  
  88. const tableObserver = new MutationObserver(() => {
  89. updateTable(tbody);
  90. });
  91. const parentObserver = new MutationObserver(async () => {
  92. tbody = document.getElementById('standings-tbody');
  93. if (tbody) {
  94. parentObserver.disconnect();
  95. await fetchLangJson();
  96. updateTable(tbody);
  97. tableObserver.observe(
  98. tbody,
  99. { childList: true }
  100. )
  101. }
  102. });
  103. parentObserver.observe(
  104. document.getElementById('vue-standings'),
  105. { childList: true, subtree: true }
  106. );
  107. })();