TheSina / Persian Font Fix (Vazir)

// ==UserScript==
// @name         Persian Font Fix (Vazir)
// @namespace    https://greasyfork.org/en/scripts/538095-persian-font-fix-vazir
// @version      1.87
// @description  Apply Vazir font to Persian/RTL content across selected websites
// @author       TheSina
// @match       *://*.telegram.org/*
// @match       *://*.x.com/*
// @match       *://*.twitter.com/*
// @match       *://*.instagram.com/*
// @match       *://*.facebook.com/*
// @match       *://*.whatsapp.com/*
// @match       *://*.github.com/*
// @match       *://*.youtube.com/*
// @match       *://*.soundcloud.com/*
// @match       *://www.google.com/*
// @match       *://gemini.google.com/*
// @match       *://translate.google.com/*
// @match       *://*.chatgpt.com/*
// @match       *://*.openai.com/*
// @match       *://fa.wikipedia.org/*
// @match       *://app.slack.com/*
// @match       *://*.goodreads.com/*
// @match       *://*.reddit.com/*
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==
/* jshint esversion: 6 */
/* global requestIdleCallback */
(function () {
  'use strict';

  // --- Font Injection (Broad Compatibility) ---
  GM_addStyle(`
    @font-face {
        font-family: 'VazirmatnFixed';
        src: local('Vazirmatn'), local('Noto Sans');
        font-display: swap;
        unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
    }

    *:not(svg):not([class~="material-icons"]):not([class~="material-icons-extended"]):not([class~="mat-icon"]):not([class~="mat-icon-no-color"]):not([class~="google-symbols-subset"]):not([class~="icon"]):not([class~="tgico"]):not([class~="c-ripple"]):not([class~="avatar"]):not([class~="avatar-icon"]):not([class~="avatar-icon-saved"]):not([class~="notranslate"]) {
        font-family: 'VazirmatnFixed', 'Noto Sans', 'Noto Color Emoji', 'Google Symbols', "Google Sans Flex", "Google Sans", "Helvetica Neue", sans-serif !important;
    }
`);

  const persianRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
  const replacementRegex = /[\u064A\u0643]/g;
  const replacements = {
    'ي': 'ی',
    'ك': 'ک'
  };

  const fixText = text =>
    persianRegex.test(text) ? text.replace(replacementRegex, c => replacements[c] || c) : text;

  const fixPersianCharsInNode = root => {
    if (!persianRegex.test(root.textContent)) return;
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
    let node;
    while ((node = walker.nextNode())) {
      if (!node.parentElement || ['SCRIPT', 'STYLE'].includes(node.parentElement.tagName)) continue;
      const original = node.nodeValue;
      const fixed = fixText(original);
      if (original !== fixed) node.nodeValue = fixed;
    }
  };

  const processInputElement = el => {
    if (el.dataset.__persianFixAttached) return;
    el.dataset.__persianFixAttached = "true";

    const fixInput = () => {
      const original = el.value;
      if (!original || !persianRegex.test(original)) return;
      const fixed = fixText(original);
      if (original !== fixed) {
        const start = el.selectionStart;
        const end = el.selectionEnd;
        el.value = fixed;
        if (start !== null && end !== null) el.setSelectionRange(start, end);
      }
    };

    if (persianRegex.test(el.value)) fixInput();
    el.addEventListener('input', () => requestIdleCallback(fixInput));
  };

  const processAllInputs = root => {
    root.querySelectorAll('input[type="text"], input[type="search"], textarea').forEach(processInputElement);
  };

  // --- Observer (with throttle) ---
  const pending = new Set();
  let throttleRunning = false;

  const runThrottle = () => {
    if (throttleRunning) return;
    throttleRunning = true;

    const applyFix = () => {
      pending.forEach(node => {
        fixPersianCharsInNode(node);
        processAllInputs(node);
      });
      pending.clear();
      throttleRunning = false;
    };

    'requestIdleCallback' in window
      ?
      requestIdleCallback(applyFix, {
        timeout: 300
      }) :
      setTimeout(applyFix, 200);
  };

  const observer = new MutationObserver(mutations => {
    for (const m of mutations) {
      m.addedNodes.forEach(node => {
        if (
          (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) &&
          document.body.contains(node)
        ) {
          if (node.textContent && persianRegex.test(node.textContent)) {
            pending.add(node);
          }
        }
      });
    }
    runThrottle();
  });

  const start = () => {
    fixPersianCharsInNode(document.body);
    processAllInputs(document);
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  };

  if (document.body) {
    start();
  }
  else {
    new MutationObserver((_, obs) => {
      if (document.body) {
        obs.disconnect();
        start();
      }
    }).observe(document.documentElement, {
      childList: true
    });
  }
})();