Persian Font Fix (Vazir)

Apply Vazir font to Persian/RTL content across selected websites

当前为 2025-06-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         Persian Font Fix (Vazir)
// @namespace    https://gf.qytechs.cn/en/scripts/538095-persian-font-fix-vazir
// @version      1.8
// @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==

(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;
        }

        body, span, div, p, a, li, td, th, input, textarea, button, [class*="text"], [class*="font"], [class*="label"] {
            font-family: 'VazirmatnFixed', 'Noto Sans', sans-serif !important;
        }
    `);

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

    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 });
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址