Leo79 / Flag Replacer

// ==UserScript==
// @name         Flag Replacer
// @version      1.9
// @description  Adjustment of countries and regions.
// @author       Leo79 (https://openuserjs.org/users/Leo79)
// @license      MIT
// @updateURL    https://openuserjs.org/meta/Leo79/Flag_Replacer.meta.js
// @downloadURL  https://openuserjs.org/install/Leo79/Flag_Replacer.user.js
// @copyright    2026, Leo79 (https://openuserjs.org/users/Leo79)
// @match        *://*.vlr.gg/*
// @run-at       document-start
// @grant        none
// ==/UserScript==
(function () {
  'use strict';

  const TW_FLAG = '';
  const CN_FLAG = '';
  const SKIPPED_TEXT_PARENT_SELECTOR = 'script, style, textarea, input, noscript';
  const TARGET_FLAG_SELECTOR = '.flag.mod-tw, .flag.mod-hk, .flag.mod-mo';
  const TEXT_REPLACEMENTS = [
    [TW_FLAG, CN_FLAG],
    [/\bTaiwan\b(?! Province, China)/gi, 'Taiwan Province, China'],
    [/\bHong Kong\b(?! SAR, China)/gi, 'Hong Kong SAR, China'],
    [/\bMaca[ou]\b(?! SAR, China)/gi, 'Macao SAR, China'],
  ];

  // --- Text replacement (idempotent via negative lookaheads) ---

  function replaceText(text, options = {}) {
    let s = text;
    for (const [pattern, replacement] of TEXT_REPLACEMENTS) {
      s = s.replace(pattern, replacement);
    }

    if (options.replaceStandaloneCountryCode) {
      s = s.replace(/^(\s*)TW(\s*)$/g, '$1Taiwan Province, China$2');
    }

    return s;
  }

  function shouldSkipTextNode(node) {
    return Boolean(node.parentElement?.closest(SKIPPED_TEXT_PARENT_SELECTOR));
  }

  function hasNearbyTaiwanFlag(node) {
    const parent = node.parentElement;
    if (!parent) return false;
    if (parent.querySelector?.('.flag.mod-tw')) return true;
    return Boolean(parent.previousElementSibling?.matches?.('.flag.mod-tw'));
  }

  function replaceInTextNode(node) {
    if (!node.nodeValue || shouldSkipTextNode(node)) return;
    const newValue = replaceText(node.nodeValue, {
      replaceStandaloneCountryCode: hasNearbyTaiwanFlag(node),
    });
    if (newValue !== node.nodeValue) {
      node.nodeValue = newValue;
    }
  }

  function walkTextNodes(root) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        return shouldSkipTextNode(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
      },
    });
    while (walker.nextNode()) {
      replaceInTextNode(walker.currentNode);
    }
  }

  // --- Flag element replacement (vlr.gg CSS class based) ---

  function fixFlagElement(el) {
    if (!el || !el.classList || !el.classList.contains('flag')) return;
    if (el.classList.contains('mod-tw')) {
      el.classList.replace('mod-tw', 'mod-cn');
      el.setAttribute('title', 'China');
    }
    else if (el.classList.contains('mod-hk')) {
      el.setAttribute('title', 'Hong Kong SAR, China');
    }
    else if (el.classList.contains('mod-mo')) {
      el.setAttribute('title', 'Macao SAR, China');
    }
  }

  function fixFlagElements(root) {
    if (!root.querySelectorAll) return;
    const flags = root.querySelectorAll(TARGET_FLAG_SELECTOR);
    for (const el of flags) {
      fixFlagElement(el);
    }
  }

  function processElement(el) {
    walkTextNodes(el);
    fixFlagElement(el);
    fixFlagElements(el);
  }

  // --- MutationObserver ---

  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'childList') {
        for (const node of m.addedNodes) {
          if (node.nodeType === Node.TEXT_NODE) {
            replaceInTextNode(node);
          }
          else if (node.nodeType === Node.ELEMENT_NODE) {
            processElement(node);
          }
        }
      }
      else if (m.type === 'characterData') {
        replaceInTextNode(m.target);
      }
      else if (m.type === 'attributes') {
        fixFlagElement(m.target);
      }
    }
  });

  // Start observing early, then let DOMContentLoaded/load do full-body passes.
  observer.observe(document.documentElement || document, {
    childList: true,
    subtree: true,
    characterData: true,
    attributes: true,
    attributeFilter: ['class'],
  });

  // --- Initialization ---

  function init() {
    if (document.body) processElement(document.body);
  }

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

  // Safety net: re-scan after all resources are loaded
  window.addEventListener('load', () => {
    if (document.body) processElement(document.body);
  }, {
    once: true
  });
})();