Nomoji (Unicode-aware, flags preserved)

Blocks all emojis except flags from being displayed on any website (text, images, CSS) — Unicode-aware, reentrancy-guarded

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Nomoji (Unicode-aware, flags preserved)
// @match       *://*/*
// @run-at      document-start
// @grant       none
// @version     0.4.0
// @namespace   https://greasyfork.org/users/1497719
// @description Blocks all emojis except flags from being displayed on any website (text, images, CSS) — Unicode-aware, reentrancy-guarded
// ==/UserScript==

(() => {
  // Unicode property-based matching
  const EMOJI_CLUSTER =
    /\p{Extended_Pictographic}(?:\uFE0F)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F)?)*?/u;
  const FLAG_SEQUENCE = /[\u{1F1E6}-\u{1F1FF}]{2}/u;

  const emojiTestRE = new RegExp(
    `(?:${FLAG_SEQUENCE.source})|(?:${EMOJI_CLUSTER.source})`,
    'u'
  );
  const emojiReplaceRE = new RegExp(
    `(${FLAG_SEQUENCE.source})|(${EMOJI_CLUSTER.source})`,
    'gu'
  );

  const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE', 'NOSCRIPT', 'TEXTAREA']);
  const EDITABLE_INPUTS = new Set(['INPUT', 'TEXTAREA']);

  const isEditable = (el) =>
    el && el.nodeType === Node.ELEMENT_NODE &&
    (EDITABLE_INPUTS.has(el.tagName) || el.isContentEditable);

  const isSkippable = (el) =>
    el && el.nodeType === Node.ELEMENT_NODE && SKIP_TAGS.has(el.tagName);

  let processing = false;

  const stripTextNode = (node) => {
    if (!node || node.nodeType !== Node.TEXT_NODE) return;
    const parent = node.parentNode;
    if (!parent || isEditable(parent) || isSkippable(parent)) return;
    const val = node.nodeValue;
    if (!val || !emojiTestRE.test(val)) return;
    node.nodeValue = val
      .replace(emojiReplaceRE, (m, flag) => (flag ? flag : ''))
      .replace(/\uFE0F/gu, ''); // remove stray VS16
  };

  const processElement = (el) => {
    if (isEditable(el) || isSkippable(el)) return;

    for (const attr of ['aria-label', 'title', 'alt']) {
      if (el.hasAttribute && el.hasAttribute(attr)) {
        const v = el.getAttribute(attr);
        if (v && emojiTestRE.test(v)) {
          el.setAttribute(
            attr,
            v.replace(emojiReplaceRE, (m, flag) => (flag ? flag : '')).replace(/\uFE0F/gu, '')
          );
        }
      }
    }

    const tag = el.tagName;

    if (tag === 'IMG' || tag === 'SVG' || tag === 'PICTURE') {
      const src = (el.getAttribute && el.getAttribute('src')) || '';
      const role = (el.getAttribute && el.getAttribute('role')) || '';
      const cls = (el.className && String(el.className)) || '';
      const looksEmoji =
        /twemoji|emoji|emojione|noto-emoji|blobcat|blobmoji/i.test(src + ' ' + role + ' ' + cls);

      if (looksEmoji) {
        el.style.setProperty('visibility', 'hidden', 'important');
        el.style.setProperty('width', '0px', 'important');
        el.style.setProperty('height', '0px', 'important');
        el.style.setProperty('overflow', 'hidden', 'important');
        return;
      }
    }

    const styleAttr = el.getAttribute && el.getAttribute('style');
    if (styleAttr && /background(-image)?:/i.test(styleAttr)) {
      let bg = '';
      try {
        bg = getComputedStyle(el).backgroundImage || '';
      } catch {}
      if (/emoji|twemoji/i.test(bg)) {
        el.style.setProperty('background-image', 'none', 'important');
      }
    }
  };

  const processSubtree = (root) => {
    if (!root) return;
    if (root.nodeType === Node.TEXT_NODE) {
      stripTextNode(root);
      return;
    }
    if (root.nodeType !== Node.ELEMENT_NODE) return;

    if (!isSkippable(root)) processElement(root);

    const walker = document.createTreeWalker(
      root,
      NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
      {
        acceptNode: (node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            return isSkippable(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
          }
          if (node.nodeType === Node.TEXT_NODE) {
            const p = node.parentNode;
            if (!p || isEditable(p) || isSkippable(p)) return NodeFilter.FILTER_REJECT;
            return NodeFilter.FILTER_ACCEPT;
          }
          return NodeFilter.FILTER_SKIP;
        }
      }
    );

    let node = walker.currentNode;
    do {
      if (node.nodeType === Node.TEXT_NODE) {
        stripTextNode(node);
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        processElement(node);
      }
      node = walker.nextNode();
    } while (node);
  };

  const init = () => {
    if (!document.body) {
      requestAnimationFrame(init);
      return;
    }
    processing = true;
    try {
      processSubtree(document.body);
    } finally {
      processing = false;
    }
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init, { once: true });
  } else {
    init();
  }

  let queue = new Set();
  let scheduled = false;

  const flush = () => {
    scheduled = false;
    if (queue.size === 0) return;
    processing = true;
    observer.disconnect();
    try {
      for (const node of queue) processSubtree(node);
    } finally {
      queue.clear();
      processing = false;
      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        characterData: true
      });
    }
  };

  const schedule = (node) => {
    if (!node) return;
    if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) {
      queue.add(node);
    }
    if (!scheduled) {
      scheduled = true;
      Promise.resolve().then(flush);
    }
  };

  const observer = new MutationObserver((mutations) => {
    if (processing) return;
    for (const m of mutations) {
      if (m.type === 'characterData') {
        schedule(m.target);
      } else if (m.type === 'childList') {
        for (const node of m.addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
            schedule(node);
          }
        }
      }
    }
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
    characterData: true
  });
})();