X 阅读增强

优化 x.com / twitter.com 阅读体验:隐藏左右栏、广告,控制媒体默认展示,并提供快捷键和可拖动悬浮面板。

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         X Reading Enhancer
// @name:zh-CN   X 阅读增强
// @namespace    https://github.com/local/x-reading-enhancer
// @version      0.4.1
// @description  Improve x.com reading by toggling side columns, ads, and media defaults.
// @description:zh-CN 优化 x.com / twitter.com 阅读体验:隐藏左右栏、广告,控制媒体默认展示,并提供快捷键和可拖动悬浮面板。
// @author       Codex
// @match        https://x.com/*
// @match        https://twitter.com/*
// @license      MIT
// @supportURL   https://github.com/Skylerliutian/x-reading-enhancer/issues
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE_PREFIX = "x-reading-enhancer.";
  const DEFAULTS = {
    leftVisible: true,
    rightVisible: true,
    hideAds: true,
    mediaVisible: true,
    panelOpen: false,
    panelPosition: null,
  };
  const DRAG_MARGIN = 8;
  const DRAG_CLICK_TOLERANCE = 4;
  const PANEL_OFFSET = 22;
  const SHORTCUTS = {
    A: "hideAds",
    L: "leftVisible",
    M: "mediaVisible",
    R: "rightVisible",
  };

  const AD_PATTERNS = [
    /^ad$/i,
    /^promoted$/i,
    /^promoted by\b/i,
    /^sponsored$/i,
    /^sponsored by\b/i,
    /^广告$/,
    /^廣告$/,
    /^推广$/,
    /^推廣$/,
    /^赞助$/,
    /^贊助$/,
    /^プロモーション$/,
    /^広告$/,
    /^プロモーション広告$/,
    /^gesponsert$/i,
    /^anzeige$/i,
    /^sponsorise$/i,
    /^sponsorise par\b/i,
    /^promocionado$/i,
    /^promovido$/i,
    /^promosso$/i,
    /^annuncio$/i,
    /^reklama$/i,
    /^реклама$/i,
  ];

  const MEDIA_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    '[data-testid="playButton"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
    'img[src*="pbs.twimg.com/media"]',
    'img[src*="pbs.twimg.com/amplify_video_thumb"]',
    "video",
  ].join(",");
  const MEDIA_BLOCK_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
  ].join(",");

  const settings = loadSettings();
  let observer = null;
  let scanQueued = false;
  let mediaToggleListenerInstalled = false;

  installStyles();
  applyRootClasses();
  onReady(() => {
    ensurePanel();
    setupMediaToggleListener();
    scanPage();
    startObserver();
  });

  function loadSettings() {
    return Object.fromEntries(
      Object.entries(DEFAULTS).map(([key, fallback]) => [key, readSetting(key, fallback)]),
    );
  }

  function readSetting(key, fallback) {
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_getValue === "function") {
        return GM_getValue(storageKey, fallback);
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      const stored = window.localStorage.getItem(storageKey);
      return stored == null ? fallback : JSON.parse(stored);
    } catch (_) {
      return fallback;
    }
  }

  function writeSetting(key, value) {
    settings[key] = value;
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(storageKey, value);
        return;
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      window.localStorage.setItem(storageKey, JSON.stringify(value));
    } catch (_) {
      // Ignore storage failures; current-page controls still work.
    }
  }

  function installStyles() {
    const css = `
      :root.xre-hide-left header[role="banner"] {
        visibility: hidden !important;
      }

      :root.xre-hide-right [data-testid="sidebarColumn"],
      :root.xre-hide-right aside[role="complementary"] {
        visibility: hidden !important;
      }

      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="tweetPhoto"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoComponent"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="gifPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="playButton"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/photo/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/video/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/media"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/amplify_video_thumb"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) video {
        display: none !important;
      }

      .xre-media-toggle {
        display: none;
        width: 100%;
        min-height: 42px;
        margin: 10px 0 2px;
        padding: 10px 12px;
        border: 1px solid rgb(47, 51, 54);
        border-radius: 8px;
        background: rgba(29, 155, 240, 0.1);
        color: #1d9bf0;
        cursor: pointer;
        font: inherit;
        font-weight: 700;
        text-align: center;
      }

      .xre-media-toggle:hover,
      .xre-media-toggle:focus-visible {
        background: rgba(29, 155, 240, 0.18);
        outline: 2px solid rgba(29, 155, 240, 0.5);
        outline-offset: 2px;
      }

      :root.xre-hide-media article[data-xre-has-media="true"]:not([data-xre-media-expanded="true"]) .xre-media-toggle {
        display: block;
      }

      :root:not(.xre-hide-media) .xre-media-toggle {
        display: none !important;
      }

      article[data-xre-ad-hidden="true"] {
        display: none !important;
      }

      #xre-root {
        --xre-panel-offset: ${PANEL_OFFSET}px;
        position: fixed;
        right: 18px;
        bottom: 18px;
        width: 55px;
        height: 55px;
        z-index: 2147483647;
        color-scheme: dark;
        color: #eff3f4;
        scrollbar-color: rgb(62, 65, 68) rgb(22, 24, 28);
        font-family: TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        font-size: 14px;
        line-height: 1.35;
        -webkit-text-size-adjust: 100%;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
      }

      #xre-root.xre-positioned {
        right: auto !important;
        bottom: auto !important;
      }

      #xre-root,
      #xre-root * {
        box-sizing: border-box;
      }

      #xre-root .xre-fab {
        display: flex;
        flex-basis: auto;
        flex-direction: column;
        flex-shrink: 0;
        align-self: flex-end;
        align-items: center;
        justify-content: center;
        width: 55px;
        height: 55px;
        min-width: 0;
        min-height: 0;
        margin: 0;
        padding: 0;
        border: 0 solid black;
        border-width: 1px;
        border-color: rgb(75, 78, 82);
        border-radius: 16px;
        background-color: rgba(0, 0, 0, 0.65);
        color: inherit;
        color-scheme: dark;
        box-shadow:
          rgba(255, 255, 255, 0.2) 0 0 15px,
          rgba(255, 255, 255, 0.15) 0 0 3px 1px;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        box-sizing: border-box;
        cursor: pointer;
        font-family: inherit;
        font-size: inherit;
        font-weight: 800;
        list-style: none;
        outline-style: none;
        pointer-events: auto !important;
        text-align: inherit;
        text-decoration: none;
        transition-duration: 0.2s;
        transition-property: background-color, box-shadow;
        z-index: 0;
        touch-action: none;
      }

      #xre-root .xre-fab:hover,
      #xre-root .xre-fab:focus-visible {
        background-color: rgba(22, 24, 28, 0.75);
        box-shadow:
          rgba(255, 255, 255, 0.28) 0 0 18px,
          rgba(255, 255, 255, 0.18) 0 0 4px 1px;
      }

      #xre-root .xre-fab-icon {
        display: block;
        width: 27px;
        height: 27px;
        color: #eff3f4;
        pointer-events: none;
      }

      #xre-root .xre-visually-hidden {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        border: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }

      #xre-root .xre-panel {
        position: absolute;
        right: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
        width: 238px;
        padding: 14px;
        border: 1px solid rgba(239, 243, 244, 0.18);
        border-radius: 8px;
        background: rgba(0, 0, 0, 0.94);
        box-shadow: 0 10px 36px rgba(0, 0, 0, 0.45);
      }

      #xre-root.xre-panel-below .xre-panel {
        top: var(--xre-panel-offset);
        right: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root.xre-panel-right .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
      }

      #xre-root.xre-panel-right.xre-panel-below .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        top: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root .xre-panel[hidden] {
        display: none !important;
      }

      #xre-root .xre-title {
        margin: 0 0 10px;
        color: #eff3f4;
        font-size: 15px;
        font-weight: 800;
        text-align: center;
        cursor: grab;
        user-select: none;
      }

      #xre-root.xre-dragging,
      #xre-root.xre-dragging * {
        cursor: grabbing !important;
        user-select: none !important;
      }

      #xre-root .xre-row {
        display: grid;
        grid-template-columns: 24px minmax(0, 1fr) 24px;
        align-items: center;
        min-height: 34px;
        gap: 12px;
        color: #eff3f4;
        cursor: pointer;
        user-select: none;
      }

      #xre-root .xre-row + .xre-row {
        border-top: 1px solid rgba(239, 243, 244, 0.12);
      }

      #xre-root .xre-row > span {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 2px;
        grid-column: 2;
        min-width: 0;
        text-align: center;
        overflow-wrap: anywhere;
      }

      #xre-root .xre-shortcut {
        color: #8b98a5;
        font-size: 11px;
        font-weight: 500;
        line-height: 1.1;
      }

      #xre-root .xre-row input {
        grid-column: 3;
        justify-self: end;
        width: 18px;
        height: 18px;
        margin: 0;
        accent-color: #1d9bf0;
      }

      @media (max-width: 720px) {
        #xre-root {
          right: 12px;
          bottom: 12px;
        }

        #xre-root .xre-panel {
          width: min(238px, calc(100vw - 24px));
        }
      }
    `;

    try {
      if (typeof GM_addStyle === "function") {
        GM_addStyle(css);
        return;
      }
    } catch (_) {
      // Fall through to a style element.
    }

    const style = document.createElement("style");
    style.id = "xre-style";
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  }

  function applyRootClasses() {
    const root = document.documentElement;
    root.classList.toggle("xre-hide-left", !settings.leftVisible);
    root.classList.toggle("xre-hide-right", !settings.rightVisible);
    root.classList.toggle("xre-hide-media", !settings.mediaVisible);
  }

  function onReady(callback) {
    if (document.body) {
      callback();
      return;
    }

    document.addEventListener("DOMContentLoaded", callback, { once: true });
  }

  function ensurePanel() {
    if (!document.body || document.getElementById("xre-root")) {
      return;
    }

    const root = document.createElement("div");
    root.id = "xre-root";
    root.innerHTML = `
      <button class="xre-fab" type="button" aria-label="X" aria-controls="xre-panel" aria-expanded="false" title="X">
        <svg class="xre-fab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
          <path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817-5.97 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path>
        </svg>
        <span class="xre-visually-hidden">X</span>
      </button>
      <section class="xre-panel" id="xre-panel" aria-label="X 阅读增强控制面板">
        <div class="xre-title">阅读控制</div>
        ${renderToggle("leftVisible", "显示左侧栏", "Shift+L")}
        ${renderToggle("rightVisible", "显示右侧栏", "Shift+R")}
        ${renderToggle("hideAds", "隐藏广告", "Shift+A")}
        ${renderToggle("mediaVisible", "默认显示媒体", "Shift+M")}
      </section>
    `;

    document.body.appendChild(root);

    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");
    const title = root.querySelector(".xre-title");
    let suppressFabClickUntil = 0;

    fab.addEventListener("click", (event) => {
      event.stopPropagation();

      if (Date.now() < suppressFabClickUntil) {
        return;
      }

      writeSetting("panelOpen", !settings.panelOpen);
      updatePanel(root);
    });

    root.addEventListener("change", (event) => {
      const input = event.target;

      if (!input || !input.matches || !input.matches("input[data-xre-key]")) {
        return;
      }

      writeSetting(input.dataset.xreKey, input.checked);
      applyRootClasses();
      scanPage();
      updatePanel(root);
    });

    document.addEventListener("keydown", (event) => {
      if (handleShortcut(event, root)) {
        return;
      }

      if (event.key !== "Escape" || panel.hidden) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
      fab.focus();
    }, true);

    document.addEventListener("click", (event) => {
      if (panel.hidden || root.contains(event.target)) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
    }, true);

    applyPanelPosition(root);
    setupPanelDrag(root, [fab, title], (handle) => {
      if (handle === fab) {
        suppressFabClickUntil = Date.now() + 250;
      }
    });
    updatePanel(root);
  }

  function renderToggle(key, label, shortcut) {
    return `
      <label class="xre-row" title="快捷键:${shortcut}">
        <span>
          <span>${label}</span>
          <span class="xre-shortcut">${shortcut}</span>
        </span>
        <input type="checkbox" data-xre-key="${key}">
      </label>
    `;
  }

  function handleShortcut(event, root) {
    if (
      !event.shiftKey ||
      event.ctrlKey ||
      event.altKey ||
      event.metaKey ||
      shouldIgnoreShortcutTarget(event.target)
    ) {
      return false;
    }

    const settingKey = SHORTCUTS[String(event.key || "").toUpperCase()];

    if (!settingKey) {
      return false;
    }

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    writeSetting(settingKey, !settings[settingKey]);
    applyRootClasses();
    scanPage();
    updatePanel(root);
    return true;
  }

  function shouldIgnoreShortcutTarget(target) {
    if (!target || !target.closest) {
      return false;
    }

    return Boolean(
      target.closest(
        'input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"]',
      ),
    );
  }

  function setupMediaToggleListener() {
    if (mediaToggleListenerInstalled) {
      return;
    }

    mediaToggleListenerInstalled = true;
    document.addEventListener("click", (event) => {
      const button = event.target?.closest?.(".xre-media-toggle");

      if (!button) {
        return;
      }

      const article = button.closest('article[data-testid="tweet"], article[role="article"]');

      if (!article) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      const expanded = article.dataset.xreMediaExpanded === "true";

      if (expanded) {
        delete article.dataset.xreMediaExpanded;
      } else {
        article.dataset.xreMediaExpanded = "true";
      }

      updateMediaToggleText(article);
    }, true);
  }

  function updatePanel(root) {
    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");

    if (fab && panel) {
      panel.hidden = !settings.panelOpen;
      fab.setAttribute("aria-expanded", String(settings.panelOpen));
    }

    root.querySelectorAll("input[data-xre-key]").forEach((input) => {
      input.checked = Boolean(settings[input.dataset.xreKey]);
    });

    updatePanelPlacement(root);
  }

  function setupPanelDrag(root, handles, onDragEnd) {
    handles.filter(Boolean).forEach((handle) => {
      handle.addEventListener("pointerdown", (event) => {
        if (event.button !== 0) {
          return;
        }

        const startRect = root.getBoundingClientRect();
        const state = {
          handle,
          pointerId: event.pointerId,
          startX: event.clientX,
          startY: event.clientY,
          startLeft: startRect.left,
          startTop: startRect.top,
          moved: false,
        };

        const onPointerMove = (moveEvent) => {
          if (moveEvent.pointerId !== state.pointerId) {
            return;
          }

          const dx = moveEvent.clientX - state.startX;
          const dy = moveEvent.clientY - state.startY;

          if (Math.hypot(dx, dy) > DRAG_CLICK_TOLERANCE) {
            state.moved = true;
          }

          setPanelPosition(root, clampPanelPosition(root, state.startLeft + dx, state.startTop + dy));
        };

        const onPointerUp = (upEvent) => {
          if (upEvent.pointerId !== state.pointerId) {
            return;
          }

          document.removeEventListener("pointermove", onPointerMove);
          document.removeEventListener("pointerup", onPointerUp);
          document.removeEventListener("pointercancel", onPointerUp);
          root.classList.remove("xre-dragging");

          if (state.moved) {
            const rect = root.getBoundingClientRect();
            const position = clampPanelPosition(root, rect.left, rect.top);
            setPanelPosition(root, position);
            writeSetting("panelPosition", position);
            onDragEnd(state.handle);
          }
        };

        event.preventDefault();
        root.classList.add("xre-dragging");
        handle.setPointerCapture?.(event.pointerId);
        document.addEventListener("pointermove", onPointerMove);
        document.addEventListener("pointerup", onPointerUp);
        document.addEventListener("pointercancel", onPointerUp);
      });
    });

    window.addEventListener("resize", () => {
      const rootElement = document.getElementById("xre-root");

      if (!rootElement) {
        return;
      }

      if (isValidPosition(settings.panelPosition)) {
        const position = clampPanelPosition(rootElement, settings.panelPosition.left, settings.panelPosition.top);
        setPanelPosition(rootElement, position);
        writeSetting("panelPosition", position);
      }

      updatePanelPlacement(rootElement);
    });
  }

  function applyPanelPosition(root) {
    if (!isValidPosition(settings.panelPosition)) {
      updatePanelPlacement(root);
      return;
    }

    setPanelPosition(root, clampPanelPosition(root, settings.panelPosition.left, settings.panelPosition.top));
  }

  function isValidPosition(position) {
    return (
      position &&
      Number.isFinite(position.left) &&
      Number.isFinite(position.top)
    );
  }

  function clampPanelPosition(root, left, top) {
    const rect = root.getBoundingClientRect();
    const width = rect.width || 55;
    const height = rect.height || 55;
    const maxLeft = Math.max(DRAG_MARGIN, window.innerWidth - width - DRAG_MARGIN);
    const maxTop = Math.max(DRAG_MARGIN, window.innerHeight - height - DRAG_MARGIN);

    return {
      left: Math.round(Math.min(Math.max(DRAG_MARGIN, left), maxLeft)),
      top: Math.round(Math.min(Math.max(DRAG_MARGIN, top), maxTop)),
    };
  }

  function setPanelPosition(root, position) {
    root.classList.add("xre-positioned");
    root.style.left = `${position.left}px`;
    root.style.top = `${position.top}px`;
    root.style.right = "auto";
    root.style.bottom = "auto";
    updatePanelPlacement(root);
  }

  function updatePanelPlacement(root) {
    const panel = root.querySelector(".xre-panel");

    if (!panel) {
      return;
    }

    const rootRect = root.getBoundingClientRect();
    const panelWidth = panel.offsetWidth || 238;
    const panelHeight = panel.offsetHeight || 210;
    const needsRightExpand = rootRect.right < panelWidth + PANEL_OFFSET + DRAG_MARGIN;
    const needsBelow = rootRect.top < panelHeight + PANEL_OFFSET + DRAG_MARGIN;

    root.classList.toggle("xre-panel-right", needsRightExpand);
    root.classList.toggle("xre-panel-below", needsBelow);
  }

  function startObserver() {
    if (observer || !document.body) {
      return;
    }

    observer = new MutationObserver(queueScan);
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function queueScan() {
    if (scanQueued) {
      return;
    }

    scanQueued = true;
    window.requestAnimationFrame(() => {
      scanQueued = false;
      ensurePanel();
      scanPage();
    });
  }

  function scanPage() {
    applyRootClasses();

    const articles = document.querySelectorAll('article[data-testid="tweet"], article[role="article"]');
    articles.forEach((article) => {
      syncArticleMediaToggle(article);

      if (settings.hideAds && isPromotedArticle(article)) {
        article.dataset.xreAdHidden = "true";
      } else {
        delete article.dataset.xreAdHidden;
      }
    });
  }

  function syncArticleMediaToggle(article) {
    const hasMedia = Boolean(article.querySelector(MEDIA_SELECTORS));
    const button = article.querySelector(".xre-media-toggle");

    if (!hasMedia) {
      delete article.dataset.xreHasMedia;
      delete article.dataset.xreMediaExpanded;
      button?.remove();
      return;
    }

    article.dataset.xreHasMedia = "true";

    if (!button) {
      insertMediaToggle(article);
    } else {
      updateMediaToggleText(article);
    }
  }

  function insertMediaToggle(article) {
    const button = document.createElement("button");
    button.className = "xre-media-toggle";
    button.type = "button";
    updateMediaToggleText(article, button);

    const mediaBlock = findMediaBlock(article);

    if (mediaBlock?.parentElement) {
      mediaBlock.parentElement.insertBefore(button, mediaBlock);
      return;
    }

    article.appendChild(button);
  }

  function findMediaBlock(article) {
    const block = article.querySelector(MEDIA_BLOCK_SELECTORS);

    if (block) {
      return block;
    }

    const media = article.querySelector(MEDIA_SELECTORS);
    return media?.closest?.(MEDIA_BLOCK_SELECTORS) || media;
  }

  function updateMediaToggleText(article, button = article.querySelector(".xre-media-toggle")) {
    if (!button) {
      return;
    }

    button.textContent = article.dataset.xreMediaExpanded === "true"
      ? "隐藏图片/视频"
      : "显示图片/视频";
  }

  function isPromotedArticle(article) {
    if (article.querySelector('[data-testid="placementTracking"]')) {
      return true;
    }

    const labelNodes = article.querySelectorAll('span, div[dir="auto"], [aria-label]');

    for (const node of labelNodes) {
      if (node.closest('[data-testid="tweetText"]')) {
        continue;
      }

      if (isPromotedText(node.textContent) || isPromotedText(node.getAttribute("aria-label"))) {
        return true;
      }
    }

    return false;
  }

  function isPromotedText(value) {
    const text = normalizeText(value);

    if (!text || text.length > 64) {
      return false;
    }

    return AD_PATTERNS.some((pattern) => pattern.test(text));
  }

  function normalizeText(value) {
    return String(value || "")
      .replace(/\s+/g, " ")
      .replace(/[\u00a0\u200b-\u200d\ufeff]/g, "")
      .trim();
  }
})();