Replace all Twitter emoji images (Twemoji) on x.com with native system emojis.
// ==UserScript==
// @name Replace Twitter/X Emojis with System Emojis
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Replace all Twitter emoji images (Twemoji) on x.com with native system emojis.
// @author Your Name (Updated)
// @match https://x.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Selector for Twitter's emoji images (covers SVG and potentially PNG)
const EMOJI_SELECTOR = 'img[src*="/emoji/v2/"][alt]'; // Added alt requirement to selector
/**
* Replaces a given Twitter emoji IMG node with a SPAN containing the native system emoji.
* @param {HTMLImageElement} imgNode The emoji image element to replace.
*/
function replaceEmojiNode(imgNode) {
// Check if already processed or lacks necessary attributes
if (imgNode.dataset.emojiReplaced || !imgNode.parentNode || !imgNode.alt) {
return;
}
const alt = imgNode.alt; // The actual emoji character(s)
const span = document.createElement('span');
span.textContent = alt; // Use the exact alt text (handles single/multiple emojis)
span.setAttribute('role', 'img'); // Accessibility: Treat span like an image
span.setAttribute('aria-label', alt); // Accessibility: Provide label
// --- Styling ---
// 1. Basic inline behavior and alignment (adjust if needed)
span.style.display = 'inline-block'; // Or 'inline' might work depending on context
span.style.verticalAlign = 'text-bottom'; // Common alignment, adjust based on visual tests
// 2. Try to match the size of the original emoji image
try {
const computedStyle = window.getComputedStyle(imgNode);
const height = computedStyle.height;
// const width = computedStyle.width; // Width can be tricky with multiple chars, primarily use height
// Use height for font size and line height for better vertical centering and scaling
if (height && height !== 'auto' && parseFloat(height) > 0) {
span.style.fontSize = height;
span.style.lineHeight = height; // Match line height to font size for vertical centering
span.style.height = height; // Explicit height
// Setting width can sometimes be problematic for multi-character emojis or variable-width system emojis.
// Let the browser determine width based on content and font size unless specific issues arise.
// span.style.width = 'auto'; // Allow natural width based on font/content
} else {
// Fallback size if computed style is unavailable or invalid
span.style.fontSize = '1em'; // Inherit from parent or use a reasonable default
span.style.lineHeight = '1'; // Normal line height
}
// Copy other potentially relevant styles (optional, can add complexity)
// span.style.margin = computedStyle.margin;
} catch (e) {
console.warn("Replace Emojis Script: Couldn't get computed style for emoji, using default size.", e);
// Apply fallback size on error
span.style.fontSize = '1em';
span.style.lineHeight = '1';
}
// Mark the original node as processed BEFORE replacing it
imgNode.dataset.emojiReplaced = 'true';
imgNode.style.display = 'none'; // Hide original immediately to prevent flash
// Replace the image node with the new span node
imgNode.parentNode.replaceChild(span, imgNode);
// console.log(`Replaced emoji: ${alt}`); // For debugging
}
/**
* Scans a given node and its descendants for emoji images and replaces them.
* @param {Node} targetNode The node to scan.
*/
function processNode(targetNode) {
if (!targetNode || !targetNode.querySelectorAll) return;
// Find all emoji images within the target node that haven't been replaced
const emojiImages = targetNode.querySelectorAll(EMOJI_SELECTOR + ':not([data-emoji-replaced="true"])');
emojiImages.forEach(replaceEmojiNode);
}
// --- Mutation Observer ---
// Observe DOM changes to catch dynamically loaded content
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
// Check if the added node itself is an emoji
if (node.nodeType === Node.ELEMENT_NODE && node.matches(EMOJI_SELECTOR)) {
replaceEmojiNode(node);
}
// Check if the added node contains emojis
else if (node.nodeType === Node.ELEMENT_NODE && node.querySelector) {
processNode(node);
}
});
}
// Optional: Handle attribute changes if emoji src/alt might change later
// else if (mutation.type === 'attributes' && mutation.attributeName === 'src' && mutation.target.matches(EMOJI_SELECTOR)) {
// // Re-evaluate if needed, but usually replacement is permanent
// // Be careful not to cause infinite loops if you modify attributes observer listens to
// }
});
});
// Start observing the document body for additions and subtree modifications
observer.observe(document.body, {
childList: true,
subtree: true
// attributes: true, // Uncomment cautiously if needed
// attributeFilter: ['src', 'alt'] // Specify attributes if uncommenting 'attributes'
});
// --- Initial Scan ---
// Process existing emojis present on the page when the script initially runs
console.log("Replace Emojis Script: Running initial scan...");
processNode(document.body);
console.log("Replace Emojis Script: Initial scan complete. Observer active.");
})();