您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); }); })();