X to Twitter Text Replacer

Replace "Post/Posts" with "Tweet/Tweets" and "X/X Corp" with "Twitter/Twitter Inc." on all X.com and Twitter.com sites

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         X to Twitter Text Replacer
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Replace "Post/Posts" with "Tweet/Tweets" and "X/X Corp" with "Twitter/Twitter Inc." on all X.com and Twitter.com sites
// @author       You
// @match        https://*.x.com/*
// @match        https://x.com/*
// @match        https://*.twitter.com/*
// @match        https://twitter.com/*
// @match        http://*.x.com/*
// @match        http://x.com/*
// @match        http://*.twitter.com/*
// @match        http://twitter.com/*
// @match        https://developer.x.com/*
// @match        https://developer.twitter.com/*
// @match        https://help.x.com/*
// @match        https://help.twitter.com/*
// @match        https://ads.x.com/*
// @match        https://ads.twitter.com/*
// @match        https://api.x.com/*
// @match        https://api.twitter.com/*
// @match        https://business.x.com/*
// @match        https://business.twitter.com/*
// @match        https://analytics.x.com/*
// @match        https://analytics.twitter.com/*
// @match        https://tweetdeck.x.com/*
// @match        https://tweetdeck.twitter.com/*
// @match        https://studio.x.com/*
// @match        https://studio.twitter.com/*
// @match        https://publish.x.com/*
// @match        https://publish.twitter.com/*
// @match        https://blog.x.com/*
// @match        https://blog.twitter.com/*
// @match        https://privacy.x.com/*
// @match        https://privacy.twitter.com/*
// @match        https://transparency.x.com/*
// @match        https://transparency.twitter.com/*
// @match        https://about.x.com/*
// @match        https://about.twitter.com/*
// @match        https://careers.x.com/*
// @match        https://careers.twitter.com/*
// @match        https://investor.x.com/*
// @match        https://investor.twitter.com/*
// @grant        none
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  // Text replacement mappings
  const replacements = {
    Post: "Tweet",
    Posts: "Tweets",
    post: "Tweet",
    posts: "Tweets",
    "X Corp": "Twitter Inc.",
    "X Corp.": "Twitter Inc.",
  };

  // Special handling for standalone "X" -> "Twitter"
  const replaceStandaloneX = (text) => {
    // Replace "X" only when it's standalone (not part of another word)
    // This regex looks for X that's either at word boundaries or surrounded by spaces/punctuation
    return text.replace(/\b(X)\b(?!\s*Corp)/g, "Twitter");
  };

  // Function to check if an element contains user-generated content
  const isUserContent = (element) => {
    // More focused list of selectors for actual user-generated content
    const userContentSelectors = [
      // Tweet content - the actual tweet text
      '[data-testid="tweetText"]',
      '[data-testid="tweetTextarea"]',

      // User input areas
      '[data-testid="dmComposerTextInput"]',
      '[data-testid="messageEntry"]',
      '[data-testid="messageText"]',
      '[role="textbox"]',
      '[contenteditable="true"]',
      'textarea',
      'input[type="text"]',
      'input[type="search"]',

      // User profiles and bios
      '[data-testid="UserDescription"]',
      '[data-testid="UserBio"]',

      // Links that are actually in tweets/content
      'a[href*="://"][data-testid*="tweet"]',
      'a[href*="://"][class*="tweet"]',

      // Code blocks and documentation (for developer sites)
      'code',
      'pre',
      '.highlight',
      '.language-*',
      '.hljs',

      // API documentation examples
      '.api-example',
      '.curl',
      '.json',
      '.xml',
    ];

    // Check if element or any parent matches user content selectors
    let current = element;
    while (current && current !== document.body) {
      for (const selector of userContentSelectors) {
        try {
          if (current.matches && current.matches(selector)) {
            return true;
          }
        } catch (e) {
          // Skip invalid selectors
          continue;
        }
      }

      // More specific class name checking - only for very specific patterns
      if (current.className && typeof current.className === 'string') {
        const className = current.className.toLowerCase();
        // Only check for very specific user content patterns
        if (className.includes('tweettext') ||
            className.includes('tweet-text') ||
            className.includes('user-bio') ||
            className.includes('dm-text') ||
            className.includes('message-text')) {
          return true;
        }
      }

      current = current.parentElement;
    }

    return false;
  };

  // More targeted check for URLs and suspicious patterns
  const containsURL = (text) => {
    // Only check for actual URLs and very specific patterns that indicate user content
    const patterns = [
      /https?:\/\/[^\s]+/i,     // Full URLs with protocol
      /www\.[a-zA-Z0-9.-]+/i,   // URLs with www
      /@[a-zA-Z0-9_]+/,         // Mentions
      /#[a-zA-Z0-9_]+/,         // Hashtags
      /[a-zA-Z0-9.-]+\.[a-z]{2,}\/[^\s]*/i, // Domain with path
      /curl\s/i,                // curl commands
      /\{[^}]*"[^"]*"[^}]*\}/,  // JSON objects
    ];

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

  // Function to replace text in a text node
  const replaceTextInNode = (textNode) => {
    if (isUserContent(textNode.parentElement)) {
      return;
    }

    // Additional safety check for URLs and user patterns
    if (containsURL(textNode.textContent)) {
      return;
    }

    let text = textNode.textContent;
    let modified = false;

    // Apply regular replacements
    for (const [original, replacement] of Object.entries(replacements)) {
      if (text.includes(original)) {
        text = text.replace(new RegExp(original, "g"), replacement);
        modified = true;
      }
    }

    // Apply standalone X replacement (only if no URLs detected)
    if (!containsURL(text)) {
      const newText = replaceStandaloneX(text);
      if (newText !== text) {
        text = newText;
        modified = true;
      }
    }

    if (modified) {
      textNode.textContent = text;
    }
  };

  // Function to process all text nodes in an element
  const processTextNodes = (element) => {
    const walker = document.createTreeWalker(
      element,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode: (node) => {
          // Skip empty text nodes and whitespace-only nodes
          if (!node.textContent.trim()) {
            return NodeFilter.FILTER_REJECT;
          }
          return NodeFilter.FILTER_ACCEPT;
        },
      },
      false
    );

    const textNodes = [];
    let node;
    while ((node = walker.nextNode())) {
      textNodes.push(node);
    }

    textNodes.forEach(replaceTextInNode);
  };

  // Initial processing when DOM is ready
  const processInitialContent = () => {
    processTextNodes(document.body || document.documentElement);
  };

  // Observer to handle dynamically loaded content
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          processTextNodes(node);
        } else if (node.nodeType === Node.TEXT_NODE) {
          replaceTextInNode(node);
        }
      });
    });
  });

  // Start observing when DOM is ready
  const startObserving = () => {
    if (document.body) {
      observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true,
      });
    } else {
      // If body isn't ready yet, try again in a bit
      setTimeout(startObserving, 100);
    }
  };

  // Initialize the script with multiple attempts for different site types
  const initialize = () => {
    // Try processing immediately
    processInitialContent();
    startObserving();

    // Also try after a delay for slower-loading sites
    setTimeout(() => {
      processInitialContent();
    }, 1000);

    // And another attempt for very slow sites
    setTimeout(() => {
      processInitialContent();
    }, 3000);
  };

  // Initialize the script
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initialize);
  } else {
    initialize();
  }

  // Also process content when the page becomes visible (for SPA navigation)
  document.addEventListener("visibilitychange", () => {
    if (!document.hidden) {
      setTimeout(processInitialContent, 500);
    }
  });

  // Handle hash changes for single-page apps
  window.addEventListener("hashchange", () => {
    setTimeout(processInitialContent, 500);
  });

  // Handle popstate for browser navigation
  window.addEventListener("popstate", () => {
    setTimeout(processInitialContent, 500);
  });
})();