Twitter Linkify Trends

Make Twitter trends links (again)

当前为 2025-04-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          Twitter Linkify Trends
// @description   Make Twitter trends links (again)
// @author        chocolateboy
// @copyright     chocolateboy
// @version       3.0.1
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL
// @include       https://mobile.x.com/
// @include       https://mobile.x.com/*
// @include       https://x.com/
// @include       https://x.com/*
// @require       https://code.jquery.com/jquery-3.7.1.slim.min.js
// @require       https://unpkg.com/[email protected]/dist/index.iife.min.js
// @require       https://unpkg.com/@chocolateboy/[email protected]/dist/polyfill.iife.min.js
// @require       https://unpkg.com/[email protected]/dist/index.umd.min.js
// @require       https://unpkg.com/[email protected]/dist/flru.min.js
// @grant         GM_log
// @run-at        document-start
// ==/UserScript==

// NOTE This file is generated from src/twitter-linkify-trends.user.ts and should not be edited directly.

"use strict";
(() => {
  // src/lib/util.ts
  var constant = (value) => (..._args) => value;

  // src/lib/observer.ts
  var INIT = { childList: true, subtree: true };
  var done = constant(false);
  var resume = constant(true);
  var observe = (...args) => {
    const [target, init, callback] = args.length === 3 ? args : args.length === 2 ? args[0] instanceof Element ? [args[0], INIT, args[1]] : [document.body, args[0], args[1]] : [document.body, INIT, args[0]];
    const onMutate = (mutations, observer2) => {
      observer2.disconnect();
      const resume2 = callback({ mutations, observer: observer2, target });
      if (resume2 !== false) {
        observer2.observe(target, init);
      }
    };
    const observer = new MutationObserver(onMutate);
    queueMicrotask(() => onMutate([], observer));
    return observer;
  };

  // src/twitter-linkify-trends.user.ts
  // @license       GPL
  var CACHE = exports.default(128);
  var DISABLED_EVENTS = "click touch";
  var EVENT_DATA = "data.explore_page.body.initialTimeline.timeline.timeline.instructions[-1].entries[1].content.items.*.item.itemContent";
  var EVENT_DATA_ENDPOINT = "/ExplorePage";
  var EVENT = 'div[role="link"][data-testid="trend"]:has([data-testid^="UserAvatar-Container"]):not([data-linked])';
  var TREND = 'div[role="link"][data-testid="trend"]:not(:has([data-testid^="UserAvatar-Container"])):not([data-linked])';
  var VIDEO = 'div[role="presentation"] div[role="link"][data-testid^="media-tweet-card-"]:not([data-linked])';
  var SELECTOR = [EVENT, TREND, VIDEO].join(", ");
  function disableAll(e) {
    e.stopPropagation();
  }
  function disableSome(e) {
    const $target = $(e.target);
    const $caret = $target.closest('[data-testid="caret"]', this);
    if (!$caret.length) {
      e.stopPropagation();
    }
  }
  function hookXHROpen(oldOpen) {
    return function open(_method, url) {
      const $url = URL.parse(url);
      if ($url.pathname.endsWith(EVENT_DATA_ENDPOINT)) {
        this.addEventListener("load", () => processEventData(this.responseText));
      }
      return GMCompat.apply(this, oldOpen, arguments);
    };
  }
  function linkFor(href) {
    return $("<a></a>").attr({ href, role: "link", "data-focusable": true }).css({ color: "inherit", textDecoration: "inherit" });
  }
  function onElement(el) {
    const $el = $(el);
    let fixPointer = true;
    let linked = true;
    if ($el.is(EVENT)) {
      $el.on(DISABLED_EVENTS, disableAll);
      linked = onEventElement($el);
    } else if ($el.is(TREND)) {
      $el.on(DISABLED_EVENTS, disableSome);
      onTrendElement($el);
    } else if ($el.is(VIDEO)) {
      fixPointer = false;
      $el.on(DISABLED_EVENTS, disableAll);
      onVideoElement($el);
    }
    if (linked) {
      $el.attr("data-linked", "true");
    }
    if (fixPointer) {
      $el.css("cursor", "auto");
    }
  }
  function onEventElement($event) {
    const { target, title } = targetFor($event);
    const url = CACHE.get(title);
    if (!url) {
      return false;
    }
    console.debug(`element (event):`, JSON.stringify(title));
    const $link = linkFor(url);
    $(target).parent().wrap($link);
    return true;
  }
  function onTrendElement($trend) {
    const { target, title } = targetFor($trend);
    const param = /\s+/.test(title) ? '"' + title.replace(/"/g, "") + '"' : title;
    console.debug("element (trend):", param);
    const query = encodeURIComponent(param);
    const url = `${location.origin}/search?q=${query}&src=trend_click&vertical=trends`;
    $(target).wrap(linkFor(url));
  }
  function onVideoElement($link) {
    const id = $link.data("testid").split("-").at(-1);
    const url = `${location.origin}/i/web/status/${id}`;
    $link.wrap(linkFor(url));
  }
  function processEventData(json) {
    const data = JSON.parse(json);
    const events = exports.get(data, EVENT_DATA, []);
    for (const event of events) {
      const title = event.name;
      const uri = event.trend_url.url;
      const url = uri.replace(/^twitter:\/\//, `${location.origin}/i/`);
      console.debug("data (event):", { title, url });
      CACHE.set(title, url);
    }
  }
  function targetFor($el) {
    const targets = $el.find('div[dir="ltr"] > span').filter((_, el) => {
      const fontWeight = Number($(el).parent().css("fontWeight") || 0);
      return fontWeight >= 700;
    });
    const target = targets.get().pop();
    const title = $(target).text().trim();
    return { target, title };
  }
  function run() {
    const target = document.getElementById("react-root");
    if (!target) {
      console.warn("can't find react-root element");
      return;
    }
    observe(target, () => {
      for (const el of $(SELECTOR)) {
        onElement(el);
      }
    });
  }
  var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
  xhrProto.open = GMCompat.export(hookXHROpen(xhrProto.open));
  $(run);
})();