Twitter Linkify Trends

Make Twitter trends links (again)

当前为 2023-12-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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       2.2.1
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL
// @include       https://twitter.com/
// @include       https://twitter.com/*
// @include       https://mobile.twitter.com/
// @include       https://mobile.twitter.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/twitter-linkify-trends.user.ts
  // @license       GPL
  var CACHE = exports.default(128);
  var DEBUG = {};
  var DISABLED_EVENTS = "click touch";
  var EVENT_DATA = "/2/guide.json";
  var EVENT_PATH = "timeline.instructions.*.addEntries.entries.*.content.timelineModule.items.*.item.content.eventSummary";
  var EVENT_HERO_PATH = "timeline.instructions.*.addEntries.entries.*.content.item.content.eventSummary";
  var LIVE_EVENT_KEY = "/lex/placeholder_live_nomargin";
  var EVENT = '[data-testid="sidebarColumn"] div[role="link"]:not([data-testid]):not([data-linked])';
  var EVENT_IMAGE = `${EVENT} > div > div:nth-child(2):last-child img[src]:not([src=""])`;
  var EVENT_HERO = 'div[role="link"][data-testid="eventHero"]:not([data-linked])';
  var EVENT_HERO_IMAGE = `${EVENT_HERO} > div:first-child [data-testid="image"] > img[src]:not([src=""])`;
  var TREND = 'div[role="link"][data-testid="trend"]:not([data-linked])';
  var VIDEO = 'div[role="presentation"] div[role="link"][data-testid^="media-tweet-card-"]:not([data-linked])';
  var EVENT_ANY = [EVENT, EVENT_HERO].join(", ");
  var SELECTOR = [EVENT_IMAGE, EVENT_HERO_IMAGE, TREND, VIDEO].join(", ");
  var pluck = exports.getter({ default: [], split: "." });
  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 = new URL(url);
      if ($url.pathname.endsWith(EVENT_DATA)) {
        this.addEventListener("load", () => processEventData(this.responseText));
      }
      return GMCompat.apply(this, oldOpen, arguments);
    };
  }
  function keyFor(url) {
    const { pathname: path } = new URL(url);
    return path === LIVE_EVENT_KEY ? path : path.split("/")[2];
  }
  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 $target;
    let type;
    if ($el.is(TREND)) {
      [$target, type] = [$el, "trend"];
      $el.on(DISABLED_EVENTS, disableSome);
      onTrendElement($el);
    } else if ($el.is(VIDEO)) {
      [$target, type] = [$el, "video"];
      $el.on(DISABLED_EVENTS, disableAll);
      onVideoElement($el);
    } else {
      const $event = $el.closest(EVENT_ANY);
      const wrapImage = $event.is(EVENT);
      [$target, type] = [$event, "event"];
      $event.on(DISABLED_EVENTS, disableAll);
      onEventElement($event, $el, { wrapImage });
    }
    $target.attr("data-linked", "true");
    if (type !== "video") {
      $target.css("cursor", "auto");
    }
    if (DEBUG[type]) {
      $target.css("backgroundColor", DEBUG[type]);
    }
  }
  function onEventElement($event, $image, options = {}) {
    const { target, title } = targetFor($event);
    const key = keyFor($image.attr("src"));
    console.debug("element (event):", JSON.stringify(title));
    const url = key === LIVE_EVENT_KEY ? CACHE.get(title) : CACHE.get(key);
    if (url) {
      const $link = linkFor(url);
      $(target).parent().wrap($link);
      if (options.wrapImage) {
        $image.wrap($link);
      }
    } else {
      console.warn("Can't find URL for event (element):", JSON.stringify(title));
    }
  }
  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 = `https://twitter.com/i/web/status/${id}`;
    $link.wrap(linkFor(url));
  }
  function processEventData(json) {
    const data = JSON.parse(json);
    const events = pluck(data, EVENT_PATH);
    const eventHero = pluck(data, EVENT_HERO_PATH);
    const $events = eventHero.concat(events);
    const nEvents = $events.length;
    if (!nEvents) {
      return;
    }
    for (const event of $events) {
      const { title, url: { url } } = event;
      const imageURL = event.image?.url;
      if (!imageURL) {
        console.warn("Can't find image for event (data):", title);
        continue;
      }
      const key = keyFor(imageURL);
      console.debug("data (event):", JSON.stringify(title));
      if (key === LIVE_EVENT_KEY) {
        CACHE.set(title, url);
      } else {
        CACHE.set(key, 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 init = { childList: true, subtree: true };
    const target = document.getElementById("react-root");
    if (!target) {
      console.warn("can't find react-root element");
      return;
    }
    const callback = (_mutations, observer) => {
      observer.disconnect();
      for (const el of $(SELECTOR)) {
        onElement(el);
      }
      observer.observe(target, init);
    };
    new MutationObserver(callback).observe(target, init);
  }
  var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
  xhrProto.open = GMCompat.export(hookXHROpen(xhrProto.open));
  $(run);
})();