🏷️ 小鱼标签 (UTags) - 为链接添加用户标签

此插件允许用户为网站的链接添加自定义标签。比如,可以给论坛的用户或帖子添加标签。

当前为 2023-07-04 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                 🏷️ UTags - Add usertags to links
// @name:zh-CN           🏷️ 小鱼标签 (UTags) - 为链接添加用户标签
// @namespace            https://utags.pipecraft.net/
// @homepage             https://github.com/utags/utags#readme
// @supportURL           https://github.com/utags/utags/issues
// @version              0.1.8
// @description          Allow users to add custom tags to links.
// @description:zh-CN    此插件允许用户为网站的链接添加自定义标签。比如,可以给论坛的用户或帖子添加标签。
// @icon                 https://utags.pipecraft.net/favicon.png
// @author               Pipecraft
// @license              MIT
// @match                https://*.v2ex.com/*
// @match                https://*/*
// @match                http://*/*
// @grant                GM_getValue
// @grant                GM_setValue
// @grant                GM_addValueChangeListener
// @grant                GM_removeValueChangeListener
// ==/UserScript==
//
//// Repository: https://github.com/utags/utags
//// Usage and screenshots: https://github.com/utags/utags
////
//// Recent Updates
//// - v0.1.6 2023.04.06
////    - Rebase on utags/browser-extension-starter@v2
//// - v0.1.5 2023.03.27
////    - 添加更多特殊标签,比如:标题党, 推广, 无聊, 忽略, 已阅, hide, 隐藏, 不再显示, 热门, 收藏, 关注, 稍后阅读
////    - 修改 www.v2ex.com 匹配规则,支持更多页面,比如:提醒系统、账户余额等
//// - v0.1.4 2023.03.20
////    - 支持给 www.v2ex.com 节点添加标签
////
;(() => {
  "use strict"
  var doc = document
  var uniq = (array) => [...new Set(array)]
  var $ = (element, selectors) =>
    element && typeof element === "object"
      ? element.querySelector(selectors)
      : doc.querySelector(element)
  var $$ = (element, selectors) =>
    element && typeof element === "object"
      ? [...element.querySelectorAll(selectors)]
      : [...doc.querySelectorAll(element)]
  var createElement = (tagName, attributes) => {
    const element = doc.createElement(tagName)
    if (attributes) {
      for (const name in attributes) {
        if (Object.hasOwn(attributes, name)) {
          const value = attributes[name]
          if (name === "textContent") {
            element[name] = value
          } else if (name === "style") {
            setStyle(element, value)
          } else {
            setAttribute(element, name, value)
          }
        }
      }
    }
    return element
  }
  var setAttribute = (element, name, value) =>
    element ? element.setAttribute(name, value) : void 0
  var setStyle = (element, values, overwrite) => {
    if (!element) {
      return
    }
    const style = element.style
    if (typeof values === "string") {
      style.cssText = overwrite ? values : style.cssText + ";" + values
      return
    }
    if (overwrite) {
      style.cssText = ""
    }
    for (const key in values) {
      if (Object.hasOwn(values, key)) {
        style[key] = values[key].replace("!important", "")
      }
    }
  }
  var isUrl = (text) => /^https?:\/\//.test(text)
  if (typeof Object.hasOwn !== "function") {
    Object.hasOwn = (instance, prop) =>
      Object.prototype.hasOwnProperty.call(instance, prop)
  }
  var content_default =
    '#utags_layer {  height: 200px;  width: 200px;  background-color: red;}.utags_ul {  display: inline;  list-style-type: none;  margin: 0px;  margin-left: 2px;  padding: 0px;  position: relative;  /*vertical-align: text-bottom;*/  line-height: 10px;}.utags_ul > li {  display: inline-flex;  align-items: center;}.utags_text_tag {  border: 1px solid red;  color: red !important;  border-radius: 3px;  padding: 1px 3px;  margin: 0px 3px;  font-size: 10px;  line-height: 10px;  font-weight: normal;  text-decoration: none;  cursor: pointer;}.utags_captain_tag,.utags_captain_tag2 {  border: none;  text-indent: -9999px;  width: 12px;  height: 12px;  padding: 0;  display: block;  background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuNSA5QzguMzI4NDMgOSA5IDguMzI4NDMgOSA3LjVDOSA2LjY3MTU3IDguMzI4NDMgNiA3LjUgNkM2LjY3MTU3IDYgNiA2LjY3MTU3IDYgNy41QzYgOC4zMjg0MyA2LjY3MTU3IDkgNy41IDlaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTIgNEMyIDIuODk1NDMgMi44OTU0MyAyIDQgMkgxMS4xNzE2QzExLjcwMiAyIDEyLjIxMDcgMi4yMTA3MSAxMi41ODU4IDIuNTg1NzlMMjEuNTg1OCAxMS41ODU4QzIyLjM2NjggMTIuMzY2OCAyMi4zNjY4IDEzLjYzMzIgMjEuNTg1OCAxNC40MTQyTDE0LjQxNDIgMjEuNTg1OEMxMy42MzMyIDIyLjM2NjggMTIuMzY2OCAyMi4zNjY4IDExLjU4NTggMjEuNTg1OEwyLjU4NTc5IDEyLjU4NThDMi4yMTA3MSAxMi4yMTA3IDIgMTEuNzAyIDIgMTEuMTcxNlY0Wk0yMC4xNzE2IDEzTDExLjE3MTYgNEg0VjExLjE3MTZMMTMgMjAuMTcxNkwyMC4xNzE2IDEzWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg==);  background-size: contain;}.utags_captain_tag {  opacity: 1%;  position: absolute;  top: 0px;  left: -2px;  padding: 0;  margin: 0;  border: none;  width: 4px;  height: 4px;  font-size: 1px;  background-color: #fff;}.utags_captain_tag:hover,.utags_captain_tag2:hover {  background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuNSA5QzguMzI4NDMgOSA5IDguMzI4NDMgOSA3LjVDOSA2LjY3MTU3IDguMzI4NDMgNiA3LjUgNkM2LjY3MTU3IDYgNiA2LjY3MTU3IDYgNy41QzYgOC4zMjg0MyA2LjY3MTU3IDkgNy41IDlaIiBmaWxsPSJyZWQiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yIDRDMiAyLjg5NTQzIDIuODk1NDMgMiA0IDJIMTEuMTcxNkMxMS43MDIgMiAxMi4yMTA3IDIuMjEwNzEgMTIuNTg1OCAyLjU4NTc5TDIxLjU4NTggMTEuNTg1OEMyMi4zNjY4IDEyLjM2NjggMjIuMzY2OCAxMy42MzMyIDIxLjU4NTggMTQuNDE0MkwxNC40MTQyIDIxLjU4NThDMTMuNjMzMiAyMi4zNjY4IDEyLjM2NjggMjIuMzY2OCAxMS41ODU4IDIxLjU4NThMMi41ODU3OSAxMi41ODU4QzIuMjEwNzEgMTIuMjEwNyAyIDExLjcwMiAyIDExLjE3MTZWNFpNMjAuMTcxNiAxM0wxMS4xNzE2IDRINFYxMS4xNzE2TDEzIDIwLjE3MTZMMjAuMTcxNiAxM1oiIGZpbGw9InJlZCIvPgo8L3N2Zz4K);}*:hover + .utags_ul .utags_captain_tag,.utags_ul:hover .utags_captain_tag,:not(a) + .utags_ul .utags_captain_tag {  opacity: 100%;  font-size: 10px;  width: 12px;  height: 12px;}/* Firefox does not support :has *//* vimium extension */html:has(#vimiumHintMarkerContainer) .utags_captain_tag {  opacity: 99%;  font-size: 10px;  width: 12px;  height: 12px;}:not(a) + .utags_ul .utags_captain_tag {  position: relative;}[data-utags_list_node*=",\u6807\u9898\u515A,"],[data-utags_list_node*=",\u63A8\u5E7F,"],[data-utags_list_node*=",\u65E0\u804A,"],[data-utags_list_node*=",\u5FFD\u7565,"],[data-utags_list_node*=",sb,"] {  opacity: 10%;}[data-utags_list_node*=",\u5DF2\u9605,"],[data-utags_list_node*=",\u65B0\u7528\u6237,"] {  opacity: 50%;}[data-utags_list_node*=",hide,"],[data-utags_list_node*=",\u9690\u85CF,"],[data-utags_list_node*=",\u4E0D\u518D\u663E\u793A,"],[data-utags_list_node*=",block,"] {  opacity: 5%;  display: none;}[data-utags_list_node*=",\u70ED\u95E8,"],[data-utags_list_node*=",\u6536\u85CF,"],[data-utags_list_node*=",\u5173\u6CE8,"],[data-utags_list_node*=",\u7A0D\u540E\u9605\u8BFB,"] {  background-image: linear-gradient(to right, #ffffff, #fefce8) !important;  opacity: 100% !important;  display: block !important;}[data-utags_list_node*=",\u70ED\u95E8,"],[data-utags_list_node*=",\u6536\u85CF,"],[data-utags_list_node*=",\u5173\u6CE8,"] {  background-image: linear-gradient(to right, #ffffff, #fef2f2) !important;}[data-utags_list_node]:hover {  opacity: 100% !important;}'
  function createTag(tagName) {
    const a = createElement("a")
    a.textContent = tagName
    a.dataset.utags_tag = tagName
    a.setAttribute(
      "href",
      "https://utags.pipecraft.net/tags/#" + encodeURIComponent(tagName)
    )
    a.setAttribute("target", "_blank")
    a.setAttribute("class", "utags_text_tag")
    return a
  }
  var site = {
    getListNodes() {
      const patterns = [".box .cell"]
      return $$(patterns.join(","))
    },
    getConditionNodes() {
      const patterns = [
        ".box .cell .topic-link",
        ".item_hot_topic_title a",
        '.box .cell .topic_info strong:first-of-type a[href*="/member/"]',
        ".box .cell .topic_info .node",
        '#Main strong a.dark[href*="/member/"]',
      ]
      return $$(patterns.join(","))
    },
    matchedNodes() {
      const patterns = [
        'a[href*="/t/"]',
        'a[href*="/member/"]',
        'a[href*="/go/"]',
        '.topic_info a[href*="/member/"]',
        "a.topic-link",
        ".box .cell .topic_info .node",
        ".item_hot_topic_title a",
        '#Main strong a.dark[href*="/member/"]',
        '.topic_content a[href*="/member/"]',
        '.topic_content a[href*="/t/"]',
        '.reply_content a[href*="/member/"]',
        '.reply_content a[href*="/t/"]',
        '.header small a[href*="/member/"]',
        '.header a[href*="/go/"]',
        '.dock_area a[href*="/member/"]',
        '.dock_area a[href*="/t/"]',
      ]
      const elements = $$(patterns.join(","))
      const excludePatterns = [
        ".site-nav a",
        ".cell_tabs a",
        ".tab-alt-container a",
        "#SecondaryTabs a",
        "a.page_normal,a.page_current",
        "a.count_livid",
      ]
      const excludeElements = new Set($$(excludePatterns.join(",")))
      function getCanonicalUrl2(url) {
        return url
          .replace(/[?#].*/, "")
          .replace(/(\w+\.)?v2ex.com/, "www.v2ex.com")
      }
      const nodes = [...elements].map((element) => {
        if (excludeElements.has(element)) {
          return {}
        }
        if (element.querySelector("img")) {
          return {}
        }
        const key = getCanonicalUrl2(element.href)
        const title = element.textContent
        const meta = { title }
        element.utags = { key, meta }
        return element
      })
      if (location.pathname.includes("/member/")) {
        const profile = $("h1")
        if (profile) {
          const key = "https://www.v2ex.com/member/" + profile.textContent
          const meta = { title: profile.textContent }
          profile.utags = { key, meta }
          nodes.push(profile)
        }
      }
      if (location.pathname.includes("/t/")) {
        const header = $(".topic_content")
        if (header) {
          const key = getCanonicalUrl2(
            "https://www.v2ex.com" + location.pathname
          )
          const title = $("h1").textContent
          const meta = { title }
          header.utags = { key, meta }
          nodes.push(header)
        }
      }
      if (location.pathname.includes("/go/")) {
        const header = $(".cell_ops.flex-one-row input")
        if (header) {
          const key = getCanonicalUrl2(
            "https://www.v2ex.com" + location.pathname
          )
          const title = document.title.replace(/.*›\s*/, "").trim()
          const meta = { title }
          header.utags = { key, meta }
          nodes.push(header)
        }
      }
      return nodes
    },
  }
  var v2ex_default = site
  function matchedSite(hostname2) {
    if (/v2ex\.com|v2hot\./.test(hostname2)) {
      return v2ex_default
    }
    return null
  }
  function getListNodes(hostname2) {
    const site2 = matchedSite(hostname2)
    if (site2) {
      return site2.getListNodes()
    }
    return []
  }
  function getConditionNodes(hostname2) {
    const site2 = matchedSite(hostname2)
    if (site2) {
      return site2.getConditionNodes()
    }
    return []
  }
  function getCanonicalUrl(url) {
    return url
  }
  function matchedNodes(hostname2) {
    const site2 = matchedSite(hostname2)
    const set = /* @__PURE__ */ new Set()
    if (site2) {
      const array2 = site2.matchedNodes()
      for (const element of array2) {
        set.add(element)
      }
    }
    const array = $$("[data-utags_primary_link]")
    for (const element of array) {
      if (!element.utags) {
        const key = getCanonicalUrl(element.href)
        const title = element.textContent
        const meta = {}
        if (!isUrl(title)) {
          meta.title = title
        }
        element.utags = { key, meta }
      }
      set.add(element)
    }
    return [...set]
  }
  var getValue = (key) => {
    const value = GM_getValue(key)
    return value && value !== "undefined" ? JSON.parse(value) : void 0
  }
  var setValue = (key, value) => {
    if (value !== void 0) GM_setValue(key, JSON.stringify(value))
  }
  var addValueChangeListener = (key, func) => {
    const listenerId = GM_addValueChangeListener(key, func)
    return () => {
      GM_removeValueChangeListener(listenerId)
    }
  }
  var extensionVersion = "0.1.6"
  var databaseVersion = 2
  var storageKey = "extension.utags.urlmap"
  var cachedUrlMap
  async function getUrlMap() {
    return (await getValue(storageKey)) || {}
  }
  async function getUrlMapVesion1() {
    return getValue("plugin.utags.tags.v1")
  }
  async function getTags(key) {
    if (!cachedUrlMap) {
      cachedUrlMap = await getUrlMap()
    }
    return cachedUrlMap[key] || { tags: [] }
  }
  async function saveTags(key, tags, meta) {
    const urlMap = await getUrlMap()
    urlMap.meta = Object.assign({}, urlMap.meta, {
      extensionVersion,
      databaseVersion,
    })
    const newTags = mergeTags(tags, [])
    if (newTags.length === 0) {
      delete urlMap[key]
    } else {
      const now = Date.now()
      const data = urlMap[key] || {}
      const newMeta = Object.assign({}, data.meta, meta, {
        updated: now,
      })
      newMeta.created = newMeta.created || now
      urlMap[key] = {
        tags: newTags,
        meta: newMeta,
      }
    }
    await setValue(storageKey, urlMap)
  }
  function addTagsValueChangeListener(func) {
    addValueChangeListener(storageKey, func)
  }
  addTagsValueChangeListener(async () => {
    cachedUrlMap = null
    await checkVersion()
  })
  async function reload() {
    console.log("Current extionsion is outdated, need reload page")
    const urlMap = await getUrlMap()
    urlMap.meta = urlMap.meta || {}
    await setValue(storageKey, urlMap)
    location.reload()
  }
  async function checkVersion() {
    cachedUrlMap = await getUrlMap()
    const meta = cachedUrlMap.meta || {}
    if (meta.extensionVersion !== extensionVersion) {
      console.log(
        "Previous extension version:",
        meta.extensionVersion,
        "current extension version:",
        extensionVersion
      )
      if (meta.extensionVersion > extensionVersion) {
      }
    }
    if (meta.databaseVersion !== databaseVersion) {
      console.log(
        "Previous database version:",
        meta.databaseVersion,
        "current database version:",
        databaseVersion
      )
      if (meta.databaseVersion > databaseVersion) {
        await reload()
        return false
      }
    }
    return true
  }
  function isValidKey(key) {
    return isUrl(key)
  }
  function isValidTags(tags) {
    return Array.isArray(tags)
  }
  function mergeTags(tags, tags2) {
    tags = tags || []
    tags2 = tags2 || []
    return uniq(
      tags
        .concat(tags2)
        .map((v) => (v ? String(v).trim() : v))
        .filter(Boolean)
    )
  }
  async function migrationData(urlMap) {
    console.log("Before migration", JSON.stringify(urlMap))
    const meta = urlMap.meta || {}
    const now = Date.now()
    const meta2 = { created: now, updated: now }
    if (!meta.databaseVersion) {
      meta.databaseVersion = 1
    }
    if (meta.databaseVersion === 1) {
      for (const key in urlMap) {
        if (!Object.hasOwn(urlMap, key)) {
          continue
        }
        if (!isValidKey(key)) {
          continue
        }
        const tags = urlMap[key]
        if (!isValidTags(tags)) {
          throw new Error("Invaid data format.")
        }
        const newTags = mergeTags(tags, [])
        if (newTags.length > 0) {
          urlMap[key] = { tags: newTags, meta: meta2 }
        } else {
          delete urlMap[key]
        }
      }
      meta.databaseVersion = 2
    }
    if (meta.databaseVersion === 2) {
    }
    urlMap.meta = meta
    console.log("After migration", JSON.stringify(urlMap))
    return urlMap
  }
  async function mergeData(urlMapNew) {
    if (typeof urlMapNew !== "object") {
      throw new TypeError("Invalid data format")
    }
    let numberOfLinks = 0
    let numberOfTags = 0
    const urlMap = await getUrlMap()
    if (
      !urlMapNew.meta ||
      urlMapNew.meta.databaseVersion !== urlMap.meta.databaseVersion
    ) {
      urlMapNew = await migrationData(urlMapNew)
    }
    if (urlMapNew.meta.databaseVersion !== urlMap.meta.databaseVersion) {
      throw new Error("Invalid database version")
    }
    for (const key in urlMapNew) {
      if (!Object.hasOwn(urlMapNew, key)) {
        continue
      }
      if (!isValidKey(key)) {
        continue
      }
      const tags = urlMapNew[key].tags || []
      const meta = urlMapNew[key].meta || {}
      if (!isValidTags(tags)) {
        throw new Error("Invaid data format.")
      }
      const orgData = urlMap[key] || { tags: [] }
      const orgTags = orgData.tags || []
      const newTags = mergeTags(orgTags, tags)
      if (newTags.length > 0) {
        const orgMeta = orgData.meta || {}
        const created = Math.min(orgMeta.created || 0, meta.created || 0)
        const updated = Math.max(orgMeta.updated || 0, meta.updated || 0)
        const newMata = Object.assign({}, orgMeta, meta, { created, updated })
        urlMap[key] = Object.assign({}, orgData, {
          tags: newTags,
          meta: newMata,
        })
        numberOfTags += Math.max(newTags.length - orgTags.length, 0)
        if (orgTags.length === 0) {
          numberOfLinks++
        }
      } else {
        delete urlMap[key]
      }
    }
    await setValue(storageKey, urlMap)
    console.log(
      `\u6570\u636E\u5DF2\u6210\u529F\u5BFC\u5165\uFF0C\u65B0\u589E ${numberOfLinks} \u6761\u94FE\u63A5\uFF0C\u65B0\u589E ${numberOfTags} \u6761\u6807\u7B7E\u3002`
    )
    return { numberOfLinks, numberOfTags }
  }
  async function migration() {
    const result = await checkVersion()
    if (!result) {
      return
    }
    cachedUrlMap = await getUrlMap()
    const meta = cachedUrlMap.meta || {}
    if (meta.databaseVersion !== databaseVersion) {
      meta.databaseVersion = meta.databaseVersion || 1
      if (meta.databaseVersion < databaseVersion) {
        console.log("Migration start")
        await saveTags("any", [])
        console.log("Migration done")
      }
    }
    const urlMapVer1 = await getUrlMapVesion1()
    if (urlMapVer1) {
      console.log(
        "Migration start: database version 1 to database version",
        databaseVersion
      )
      const result2 = await mergeData(urlMapVer1)
      if (result2) {
        await setValue("plugin.utags.tags.v1", null)
      }
    }
  }
  var hostname = location.hostname
  var getStyle = () => {
    const style = createElement("style")
    style.id = "utags_style"
    style.textContent = content_default
    document.head.append(style)
  }
  function appendTagsToPage(element, key, tags, meta) {
    var _a, _b
    if (
      (_b = (_a = element.nextSibling) == null ? void 0 : _a.classList) == null
        ? void 0
        : _b.contains("utags_ul")
    ) {
      element.nextSibling.remove()
    }
    const ul = createElement("ul")
    let li = createElement("li")
    let a = createElement("a")
    a.textContent = "\u{1F3F7}\uFE0F"
    a.setAttribute(
      "class",
      tags.length === 0
        ? "utags_text_tag utags_captain_tag"
        : "utags_text_tag utags_captain_tag2"
    )
    a.addEventListener("click", async function () {
      const newTags = prompt(
        "[UTags] \u8BF7\u8F93\u5165\u6807\u7B7E\uFF0C\u7528\u9017\u53F7\u5206\u5F00\u591A\u4E2A\u6807\u7B7E",
        tags.join(", ")
      )
      if (newTags !== null) {
        const newTagsArray = newTags.split(/\s*[,,]\s*/)
        await saveTags(key, newTagsArray, meta)
      }
    })
    li.append(a)
    ul.append(li)
    for (const tag of tags) {
      li = createElement("li")
      a = createTag(tag)
      li.append(a)
      ul.append(li)
    }
    ul.setAttribute("class", "utags_ul")
    element.after(ul)
  }
  async function displayTags() {
    const conditionNodes = getConditionNodes(hostname)
    for (const node of conditionNodes) {
      node.dataset.utags_condition_node = ""
    }
    const nodes = matchedNodes(hostname)
    await Promise.all(
      nodes.map(async (node) => {
        if (!node.utags || !node.utags.key) {
          return
        }
        const object = await getTags(node.utags.key)
        const tags = object.tags || []
        appendTagsToPage(node, node.utags.key, tags, node.utags.meta)
      })
    )
    const listNodes = getListNodes(hostname)
    for (const node of listNodes) {
      const tags = node.querySelectorAll(
        "[data-utags_condition_node] + .utags_ul > li > .utags_text_tag[data-utags_tag]"
      )
      if (tags.length > 0) {
        node.dataset.utags_list_node =
          [...tags].reduce(
            (accumulator, tag) => accumulator + "," + tag.textContent,
            ""
          ) + ","
      } else {
        node.dataset.utags_list_node = ""
      }
    }
  }
  async function outputData() {
    if (
      /^(utags\.pipecraft\.net|localhost|127\.0\.0\.1)$/.test(location.hostname)
    ) {
      const urlMap = await getUrlMap()
      const textarea = createElement("textarea")
      textarea.id = "utags_output"
      textarea.setAttribute("style", "display:none")
      textarea.value = JSON.stringify(urlMap)
      document.body.append(textarea)
      textarea.addEventListener("click", async () => {
        if (textarea.dataset.utags_type === "export") {
          const urlMap2 = await getUrlMap()
          textarea.value = JSON.stringify(urlMap2)
          textarea.dataset.utags_type = "export_done"
          textarea.click()
        } else if (textarea.dataset.utags_type === "import") {
          const data = textarea.value
          try {
            const result = await mergeData(JSON.parse(data))
            textarea.value = JSON.stringify(result)
            textarea.dataset.utags_type = "import_done"
            textarea.click()
          } catch (error) {
            console.error(error)
            textarea.value = JSON.stringify(error)
            textarea.dataset.utags_type = "import_failed"
            textarea.click()
          }
        }
      })
    }
  }
  async function initStorage() {
    await migration()
    addTagsValueChangeListener(displayTags)
  }
  var countOfLinks = 0
  async function main() {
    if ($("#utags_style")) {
      console.log(
        `[UTags] [${"userscript"}-${"prod"}] Skip this, since another instance is already running.`,
        location.href
      )
      return
    }
    document.addEventListener("mouseover", (event) => {
      if (event.target && event.target.tagName === "A") {
      }
    })
    getStyle()
    setTimeout(outputData, 1)
    await initStorage()
    await displayTags()
    countOfLinks = $$("a:not(.utags_text_tag)").length
    setInterval(async () => {
      const count = $$("a:not(.utags_text_tag)").length
      if (countOfLinks !== count) {
        countOfLinks = count
        await displayTags()
      }
    }, 1e3)
  }
  main()
})()