跳转到内容

User:SuperGrey/gadgets/Reaction/main.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/Reaction]]
// <nowiki>
(() => {
  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);

  // src/state.ts
  var State = class {
    constructor() {
      /**
       * 使用者名稱,從MediaWiki配置中獲取。
       * @type {string}
       * @constant
       */
      __publicField(this, "userName", mw.config.get("wgUserName"));
      /**
       * 頁面名稱,從MediaWiki配置中獲取。
       * @type {string}
       * @constant
       */
      __publicField(this, "pageName", mw.config.get("wgPageName"));
      // MediaWiki API 實例
      __publicField(this, "_api", null);
      // 簡繁轉換
      __publicField(this, "convByVar", function(langDict) {
        if (langDict && langDict.hant) {
          return langDict.hant;
        }
        return "繁簡轉換未初始化,且 langDict 無效!";
      });
    }
    getApi() {
      if (!this._api) {
        this._api = new mw.Api({ "User-Agent": "Reaction/1.0.0" });
      }
      return this._api;
    }
    initHanAssist() {
      return mw.loader.using("ext.gadget.HanAssist").then((require2) => {
        const { convByVar } = require2("ext.gadget.HanAssist");
        if (typeof convByVar === "function") {
          this.convByVar = convByVar;
        }
      });
    }
  };
  var state = new State();
  var state_default = state;

  // src/utils.ts
  var chineseUtcRegex = `\\d{4}年\\d{1,2}月\\d{1,2}日 \\([日一二三四五六]\\) \\d{1,2}:\\d{2} \\(UTC\\)`;
  function escapeRegex(string) {
    return mw.util.escapeRegExp(string);
  }
  function atChineseUtcRegex() {
    return "(?:|[於于]" + chineseUtcRegex + ")";
  }
  function userNameAtChineseUtcRegex() {
    return escapeRegex(state_default.userName) + atChineseUtcRegex();
  }
  function getCurrentChineseUtc() {
    const date = /* @__PURE__ */ new Date();
    return dateToChineseUtc(date);
  }
  function parseUtc14(utc14) {
    const year = Number(utc14.slice(0, 4));
    const month = Number(utc14.slice(4, 6)) - 1;
    const day = Number(utc14.slice(6, 8));
    const hour = Number(utc14.slice(8, 10));
    const minute = Number(utc14.slice(10, 12));
    const second = Number(utc14.slice(12, 14));
    return new Date(Date.UTC(year, month, day, hour, minute, second));
  }
  function utc14ToChineseUtc(utc14) {
    const date = parseUtc14(utc14);
    return dateToChineseUtc(date);
  }
  function dateToChineseUtc(date) {
    return date.getUTCFullYear() + "年" + (date.getUTCMonth() + 1) + "月" + date.getUTCDate() + "日 (" + [
      "日",
      "一",
      "二",
      "三",
      "四",
      "五",
      "六"
    ][date.getUTCDay()] + ") " + date.getUTCHours().toString().padStart(2, "0") + ":" + date.getUTCMinutes().toString().padStart(2, "0") + " (UTC)";
  }
  function parseTimestamp(timestamp) {
    let utcTimestamp = timestamp.querySelector(".localcomments");
    if (utcTimestamp) {
      return utcTimestamp.getAttribute("title");
    } else {
      let href = timestamp.getAttribute("href");
      let ts_s = href.split("#")[1] || "";
      if (ts_s.startsWith("c-")) {
        let ts = (ts_s.match(/-(\d{14})/) || [])[1];
        if (ts) {
          return utc14ToChineseUtc(ts);
        }
        ts = (ts_s.match(/-(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z)/) || [])[1];
        if (ts) {
          let date = new Date(ts);
          return dateToChineseUtc(date);
        }
      }
      console.error("[Reaction] Unable to parse timestamp in: " + href);
      return null;
    }
  }

  // src/api.ts
  async function retrieveFullText() {
    let response = await state_default.getApi().get({
      action: "query",
      titles: state_default.pageName,
      prop: "revisions",
      rvslots: "*",
      rvprop: "content",
      indexpageids: 1
    });
    let fulltext = response.query.pages[response.query.pageids[0]].revisions[0].slots.main["*"];
    return fulltext + "\n";
  }
  async function saveFullText(fulltext, summary) {
    try {
      await state_default.getApi().postWithToken("edit", {
        action: "edit",
        title: state_default.pageName,
        text: fulltext,
        summary: summary + " ([[User:SuperGrey/gadgets/Reaction|Reaction]])"
      });
      mw.notify(state_default.convByVar({ hant: "[Reaction] 儲存成功!", hans: "[Reaction] 保存成功!" }), {
        title: "成功",
        type: "success"
      });
      return true;
    } catch (e) {
      console.error(e);
      mw.notify(state_default.convByVar({
        hant: "[Reaction] 失敗!無法儲存頁面。",
        hans: "[Reaction] 失败!无法保存页面。"
      }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
      return false;
    }
  }
  async function modifyPage(mod) {
    let fulltext;
    try {
      fulltext = await retrieveFullText();
    } catch (e) {
      console.error(e);
      mw.notify(state_default.convByVar({
        hant: "[Reaction] 失敗!無法獲取頁面內容。",
        hans: "[Reaction] 失败!无法获取页面内容。"
      }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
      return false;
    }
    let newFulltext;
    let summary = "";
    try {
      let timestampRegex = new RegExp(`${escapeRegex(mod.timestamp)}`, "g");
      let timestampMatch = fulltext.match(timestampRegex);
      if (!timestampMatch || timestampMatch.length === 0) {
        console.log("[Reaction] Unable to find timestamp " + mod.timestamp + " in: " + fulltext);
        throw new Error("[Reaction] " + state_default.convByVar({
          hant: "原文中找不到時間戳:",
          hans: "原文中找不到时间戳:"
        }) + mod.timestamp);
      }
      if (timestampMatch.length > 1) {
        console.log("[Reaction] More than one timestamp found: " + timestampMatch);
        throw new Error("[Reaction] " + state_default.convByVar({
          hant: "原文中找到多個相同的時間戳,小工具無法處理:",
          hans: "原文中找到多个相同的时间戳,小工具无法处理:"
        }) + mod.timestamp + state_default.convByVar({
          hant: "。請手動編輯。",
          hans: "。请手动编辑。"
        }));
      }
      let pos = fulltext.search(timestampRegex);
      console.log("[Reaction] Found timestamp " + mod.timestamp + " at position " + pos);
      if (mod.remove) {
        let regex = new RegExp(` *\\{\\{ *[Rr]eact(?:ion|) *\\| *${escapeRegex(mod.remove)} *\\| *${userNameAtChineseUtcRegex()} *}}`, "g");
        let lineEnd = fulltext.indexOf("\n", pos);
        let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
        let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, "");
        newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
        summary = "− " + mod.remove;
      } else if (mod.downvote) {
        let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${escapeRegex(mod.downvote)} *(|\\|[^}]*?)\\| *${userNameAtChineseUtcRegex()} *(|\\|[^}]*?)}}`, "g");
        let lineEnd = fulltext.indexOf("\n", pos);
        let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
        let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, `{{Reaction|${mod.downvote}$1$2}}`);
        newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
        summary = "− " + mod.downvote;
      } else if (mod.upvote) {
        let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${escapeRegex(mod.upvote)}([^}]*?)}}`, "g");
        let lineEnd = fulltext.indexOf("\n", pos);
        let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
        let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, `{{Reaction|${mod.upvote}$1|${state_default.userName}${getCurrentChineseUtc()}}}`);
        newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
        summary = "+ " + mod.upvote;
      } else if (mod.append) {
        let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${escapeRegex(mod.append)}([^}]*?)}}`, "g");
        let lineEnd = fulltext.indexOf("\n", pos);
        let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
        if (regex.test(timestamp2LineEnd)) {
          console.log("[Reaction] Reaction of " + mod.append + " already exists in: " + timestamp2LineEnd);
          throw new Error("[Reaction] " + state_default.convByVar({
            hant: "原文中已經有這個反應!",
            hans: "原文中已经有这个反应!"
          }));
        }
        let newText = "{{Reaction|" + mod.append + "|" + state_default.userName + "於" + getCurrentChineseUtc() + "}}";
        newFulltext = fulltext.slice(0, lineEnd) + " " + newText + fulltext.slice(lineEnd);
        summary = "+ " + mod.append;
      }
      if (newFulltext === fulltext) {
        console.log("[Reaction] Nothing is modified. Could be because using a template inside {{Reaction}}.");
        throw new Error("[Reaction] " + state_default.convByVar({
          hant: "原文未被修改。可能是因為使用了嵌套模板;請手動編輯。",
          hans: "原文未被修改。可能是因为使用了嵌套模板;请手动编辑。"
        }));
      }
      return await saveFullText(newFulltext, summary);
    } catch (e) {
      console.error(e);
      mw.notify(e.message, { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
      return false;
    }
  }

  // src/dom.ts
  var _handlerRegistry = /* @__PURE__ */ new WeakMap();
  var _buttonTimestamps = /* @__PURE__ */ new WeakMap();
  var timestamps = null;
  var replyButtons = null;
  function handleReactionClick(button) {
    if (button.classList.contains("reaction-new")) {
      addNewReaction(button);
    } else {
      if (button.getAttribute("data-reaction-icon-invalid")) {
        mw.notify(state_default.convByVar({
          hant: "[Reaction] 反應圖示無效,小工具無法處理。",
          hans: "[Reaction] 反应图示无效,小工具无法处理。"
        }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
        console.error("[Reaction] Invalid reaction icon.");
        return;
      }
      if (typeof window.ujsReactionConfirmedRequired !== "undefined" && window.ujsReactionConfirmedRequired) {
        let confirmMessage;
        if (button.classList.contains("reaction-reacted")) {
          confirmMessage = state_default.convByVar({
            hant: "[Reaction] 確定要取消這個反應嗎?",
            hans: "[Reaction] 确定要取消这个反应吗?"
          });
        } else {
          confirmMessage = state_default.convByVar({
            hant: "[Reaction] 確定要追加這個反應嗎?",
            hans: "[Reaction] 确定要追加这个反应吗?"
          });
        }
        OO.ui.confirm(confirmMessage, {
          title: state_default.convByVar({ hant: "確認", hans: "确认" }),
          size: "small"
        }).then((confirmed) => {
          if (confirmed) {
            toggleReaction(button);
          }
        });
      } else {
        toggleReaction(button);
      }
    }
  }
  function toggleReaction(button) {
    if (button.classList.contains("reaction-reacted")) {
      if (!button.getAttribute("data-reaction-commentors").includes(state_default.userName)) {
        mw.notify(state_default.convByVar({
          hant: "[Reaction] 失敗!不能取消並未做出的反應。",
          hans: "[Reaction] 失败!不能取消并未做出的反应。"
        }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
        console.log("[Reaction] Should not happen! " + state_default.userName + " should be in " + button.getAttribute("data-reaction-commentors"));
        return;
      }
      let buttonIcon = button.querySelector(".reaction-icon");
      let buttonCounter = button.querySelector(".reaction-counter");
      let count = parseInt(button.getAttribute("data-reaction-count") || buttonCounter.innerText);
      let mod;
      if (count > 1) {
        mod = {
          timestamp: parseTimestamp(_buttonTimestamps.get(button)),
          downvote: button.getAttribute("data-reaction-icon").trim() || buttonIcon.innerText.trim()
        };
      } else {
        mod = {
          timestamp: parseTimestamp(_buttonTimestamps.get(button)),
          remove: button.getAttribute("data-reaction-icon").trim() || buttonIcon.innerText.trim()
        };
      }
      modifyPage(mod).then((response) => {
        if (response) {
          button.classList.remove("reaction-reacted");
          if (count > 1) {
            buttonCounter.innerText = (count - 1).toString();
            let dataCommentors = button.getAttribute("data-reaction-commentors") + "/";
            dataCommentors = dataCommentors.replace(new RegExp(userNameAtChineseUtcRegex() + "/", "g"), "");
            dataCommentors = dataCommentors.slice(0, -1);
            button.setAttribute("data-reaction-commentors", dataCommentors);
            let buttonTitle = button.getAttribute("title");
            if (buttonTitle) {
              buttonTitle = buttonTitle.replace(new RegExp(userNameAtChineseUtcRegex(), "g"), "");
              let trailingSemicolonRegex = new RegExp(";" + atChineseUtcRegex() + "回[應应]了[這这][條条]留言$", "g");
              buttonTitle = buttonTitle.replace(trailingSemicolonRegex, "");
              let trailingCommaRegex = new RegExp("、​" + atChineseUtcRegex() + "(|、​.+?)(回[應应]了[這这][條条]留言)$", "g");
              buttonTitle = buttonTitle.replace(trailingCommaRegex, "$1$2");
              buttonTitle = buttonTitle.replace(new RegExp("^" + atChineseUtcRegex() + "、​"), "");
              button.setAttribute("title", buttonTitle);
            }
          } else {
            button.parentNode.removeChild(button);
          }
        }
      });
    } else {
      if (button.getAttribute("data-reaction-commentors").includes(state_default.userName)) {
        mw.notify(state_default.convByVar({
          hant: "[Reaction] 失敗!不能重複做出反應。",
          hans: "[Reaction] 失败!不能重复做出反应。"
        }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
        console.log("[Reaction] Should not happen! " + state_default.userName + " should not be in " + button.getAttribute("data-reaction-commentors"));
        return;
      }
      let buttonIcon = button.querySelector(".reaction-icon");
      let mod = {
        timestamp: parseTimestamp(_buttonTimestamps.get(button)),
        upvote: button.getAttribute("data-reaction-icon").trim() || buttonIcon.innerText.trim()
      };
      modifyPage(mod).then((response) => {
        if (response) {
          button.classList.add("reaction-reacted");
          let buttonCounter = button.querySelector(".reaction-counter");
          let count = parseInt(buttonCounter.innerText);
          buttonCounter.innerText = (count + 1).toString();
          let dataCommentors = button.getAttribute("data-reaction-commentors");
          if (dataCommentors) {
            dataCommentors += "/" + state_default.userName + "於" + getCurrentChineseUtc();
          } else {
            dataCommentors = state_default.userName + "於" + getCurrentChineseUtc();
          }
          button.setAttribute("data-reaction-commentors", dataCommentors);
          let buttonTitle = button.getAttribute("title");
          if (buttonTitle) {
            buttonTitle += ";";
          } else {
            buttonTitle = "";
          }
          buttonTitle += state_default.userName + state_default.convByVar({
            hant: "於",
            hans: "于"
          }) + getCurrentChineseUtc() + state_default.convByVar({
            hant: "回應了這條留言",
            hans: "回应了这条留言"
          });
          button.setAttribute("title", buttonTitle);
        }
      });
    }
  }
  function cancelNewReaction(button, event) {
    if (event) {
      event.stopPropagation();
    }
    let saveButton = button.querySelector(".reaction-save");
    const saveButtonClickHandler = _handlerRegistry.get(saveButton);
    if (saveButtonClickHandler) {
      saveButton.removeEventListener("click", saveButtonClickHandler);
      _handlerRegistry.delete(saveButton);
    }
    let cancelButton = button.querySelector(".reaction-cancel");
    const cancelButtonClickHandler = _handlerRegistry.get(cancelButton);
    if (cancelButtonClickHandler) {
      cancelButton.removeEventListener("click", cancelButtonClickHandler);
      _handlerRegistry.delete(cancelButton);
    }
    let buttonIcon = button.querySelector(".reaction-icon");
    buttonIcon.textContent = "+";
    let buttonCounter = button.querySelector(".reaction-counter");
    buttonCounter.innerText = state_default.convByVar({ hant: "反應", hans: "反应" });
    if (_handlerRegistry.has(button)) {
      console.error("[Reaction] Not possible! The event handler should not be registered yet.");
      return;
    }
    const buttonClickHandler = handleReactionClick.bind(this, button);
    _handlerRegistry.set(button, buttonClickHandler);
    button.addEventListener("click", buttonClickHandler);
  }
  function saveNewReaction(button, event) {
    if (event) {
      event.stopPropagation();
    }
    let input = button.querySelector(".reaction-icon input");
    if (!input.value.trim()) {
      mw.notify(state_default.convByVar({
        hant: "[Reaction] 反應內容不能為空!",
        hans: "[Reaction] 反应内容不能为空!"
      }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
      return;
    }
    let timestamp = parseTimestamp(_buttonTimestamps.get(button));
    if (!timestamp) {
      mw.notify(state_default.convByVar({
        hant: "[Reaction] 失敗!無法獲取時間戳。",
        hans: "[Reaction] 失败!无法获取时间戳。"
      }), { title: state_default.convByVar({ hant: "錯誤", hans: "错误" }), type: "error" });
      return;
    }
    let mod = {
      timestamp,
      append: input.value.trim()
    };
    modifyPage(mod).then((response) => {
      if (response) {
        button.classList.remove("reaction-new");
        button.classList.add("reaction-reacted");
        let buttonIcon = button.querySelector(".reaction-icon");
        buttonIcon.textContent = input.value;
        let buttonCounter = button.querySelector(".reaction-counter");
        buttonCounter.textContent = "1";
        button.setAttribute("title", state_default.userName + state_default.convByVar({
          hant: "於",
          hans: "于"
        }) + getCurrentChineseUtc() + state_default.convByVar({
          hant: "回應了這條留言",
          hans: "回应了这条留言"
        }));
        button.setAttribute("data-reaction-commentors", state_default.userName);
        let saveButton = button.querySelector(".reaction-save");
        const saveButtonClickHandler = _handlerRegistry.get(saveButton);
        if (saveButtonClickHandler) {
          saveButton.removeEventListener("click", saveButtonClickHandler);
          _handlerRegistry.delete(saveButton);
        }
        let cancelButton = button.querySelector(".reaction-cancel");
        const cancelButtonClickHandler = _handlerRegistry.get(cancelButton);
        if (cancelButtonClickHandler) {
          cancelButton.removeEventListener("click", cancelButtonClickHandler);
          _handlerRegistry.delete(cancelButton);
        }
        let newReactionButton = NewReactionButton();
        button.parentNode.insertBefore(newReactionButton, button.nextSibling);
        _buttonTimestamps.set(newReactionButton, _buttonTimestamps.get(button));
        if (_handlerRegistry.has(button)) {
          console.error("Not possible! The event handler should not be registered yet.");
          return;
        }
        const buttonClickHandler = handleReactionClick.bind(this, button);
        _handlerRegistry.set(button, buttonClickHandler);
        button.addEventListener("click", buttonClickHandler);
      }
    });
  }
  function ResizableInput(text = "", parent = document.body) {
    let input = document.createElement("input");
    input.value = text;
    input.style.width = "1em";
    input.style.background = "transparent";
    input.style.border = "0";
    input.style.boxSizing = "content-box";
    parent.appendChild(input);
    let hiddenInput = document.createElement("span");
    hiddenInput.style.position = "absolute";
    hiddenInput.style.top = "0";
    hiddenInput.style.left = "0";
    hiddenInput.style.visibility = "hidden";
    hiddenInput.style.height = "0";
    hiddenInput.style.overflow = "scroll";
    hiddenInput.style.whiteSpace = "pre";
    parent.appendChild(hiddenInput);
    const inputStyles = window.getComputedStyle(input);
    [
      "fontFamily",
      "fontSize",
      "fontWeight",
      "fontStyle",
      "letterSpacing",
      "textTransform"
    ].forEach((prop) => {
      hiddenInput.style[prop] = inputStyles[prop];
    });
    function inputResize() {
      hiddenInput.innerText = input.value || input.placeholder || text;
      const width = hiddenInput.scrollWidth;
      input.style.width = width + 2 + "px";
    }
    input.addEventListener("input", inputResize);
    inputResize();
    return input;
  }
  function addNewReaction(button) {
    const buttonClickHandler = _handlerRegistry.get(button);
    if (buttonClickHandler) {
      button.removeEventListener("click", buttonClickHandler);
      _handlerRegistry.delete(button);
    }
    let buttonIcon = button.querySelector(".reaction-icon");
    buttonIcon.textContent = "";
    let input = ResizableInput("👍", buttonIcon);
    input.focus();
    input.select();
    input.addEventListener("keydown", (event) => {
      if (event.key === "Enter") {
        saveNewReaction(button, false);
      } else if (event.key === "Escape") {
        cancelNewReaction(button, false);
      }
    });
    let buttonCounter = button.querySelector(".reaction-counter");
    let saveButton = document.createElement("span");
    saveButton.className = "reaction-save";
    saveButton.innerText = state_default.convByVar({ hant: "儲存", hans: "保存" });
    if (_handlerRegistry.has(saveButton)) {
      return;
    }
    const saveButtonClickHandler = saveNewReaction.bind(this, button);
    _handlerRegistry.set(saveButton, saveButtonClickHandler);
    saveButton.addEventListener("click", saveButtonClickHandler);
    let cancelButton = document.createElement("span");
    cancelButton.className = "reaction-cancel";
    cancelButton.innerText = state_default.convByVar({ hant: "取消", hans: "取消" });
    if (_handlerRegistry.has(cancelButton)) {
      return;
    }
    const cancelButtonClickHandler = cancelNewReaction.bind(this, button);
    _handlerRegistry.set(cancelButton, cancelButtonClickHandler);
    cancelButton.addEventListener("click", cancelButtonClickHandler);
    buttonCounter.innerText = "";
    buttonCounter.appendChild(saveButton);
    buttonCounter.appendChild(document.createTextNode(" | "));
    buttonCounter.appendChild(cancelButton);
  }
  function NewReactionButton() {
    let button = document.createElement("span");
    button.className = "reactionable template-reaction reaction-new";
    let buttonContent = document.createElement("span");
    buttonContent.className = "reaction-content";
    let buttonIconContainer = document.createElement("span");
    buttonIconContainer.className = "reaction-icon-container";
    let buttonIcon = document.createElement("span");
    buttonIcon.className = "reaction-icon";
    buttonIcon.innerText = "+";
    buttonIconContainer.appendChild(buttonIcon);
    let buttonCounterContainer = document.createElement("span");
    buttonCounterContainer.className = "reaction-counter-container";
    let buttonCounter = document.createElement("span");
    buttonCounter.className = "reaction-counter";
    buttonCounter.innerText = state_default.convByVar({ hant: "反應", hans: "反应" });
    buttonCounterContainer.appendChild(buttonCounter);
    buttonContent.appendChild(buttonIconContainer);
    buttonContent.appendChild(buttonCounterContainer);
    button.appendChild(buttonContent);
    let buttonClickHandler = handleReactionClick.bind(this, button);
    _handlerRegistry.set(button, buttonClickHandler);
    button.addEventListener("click", buttonClickHandler);
    return button;
  }
  function bindEvent2ReactionButton(button) {
    if (_handlerRegistry.has(button)) {
      return;
    }
    let buttonClickHandler = handleReactionClick.bind(this, button);
    _handlerRegistry.set(button, buttonClickHandler);
    button.addEventListener("click", buttonClickHandler);
    let reacted = false;
    for (const commentor of button.getAttribute("data-reaction-commentors").split("/")) {
      let regex = new RegExp("^" + userNameAtChineseUtcRegex() + "$");
      if (regex.test(commentor)) {
        reacted = true;
        break;
      }
    }
    if (reacted) {
      button.classList.add("reaction-reacted");
    }
  }
  function addReactionButtons() {
    if (document.querySelector("#reaction-finished-loading")) {
      return;
    }
    timestamps = document.querySelectorAll("a.ext-discussiontools-init-timestamplink");
    replyButtons = document.querySelectorAll("span.ext-discussiontools-init-replylink-buttons");
    for (let i = 0; i < timestamps.length; i++) {
      let timestamp = timestamps[i];
      let replyButton = replyButtons[i];
      let button = timestamp.nextElementSibling;
      while (button && button !== replyButton) {
        if (button.classList.contains("template-reaction") && button.attributes["data-reaction-commentors"]) {
          _buttonTimestamps.set(button, timestamp);
          bindEvent2ReactionButton(button);
        }
        button = button.nextElementSibling;
      }
    }
    for (let i = 0; i < replyButtons.length; i++) {
      let reactionButton = NewReactionButton();
      let timestamp = timestamps[i];
      _buttonTimestamps.set(reactionButton, timestamp);
      let replyButton = replyButtons[i];
      replyButton.parentNode.insertBefore(reactionButton, replyButton);
    }
    console.log(`[Reaction] Added ${replyButtons.length} new reaction buttons.`);
    let finishedLoading = document.createElement("div");
    finishedLoading.id = "reaction-finished-loading";
    finishedLoading.style.display = "none";
    document.querySelector("#mw-content-text .mw-parser-output").appendChild(finishedLoading);
  }

  // src/main.ts
  function init() {
    mw.loader.load("/w/index.php?title=Template:Reaction/styles.css&action=raw&ctype=text/css", "text/css");
    state_default.initHanAssist().then(() => {
      mw.hook("wikipage.content").add(function() {
        setTimeout(() => addReactionButtons(), 200);
      });
    });
  }
  init();
})();
// </nowiki>