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.7
// @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/*
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==


(function () {
    'use strict';

    // --- Style Injection ---
    GM_addStyle(`
        @font-face {
            font-family: 'VazirmatnCustom';
            src: local('Vazirmatn');
            unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
        }

        html, body, p, span, div, h1, h2, h3, h4, h5, h6, a, li, ul, ol, td, th, input, textarea {
            font-family: 'VazirmatnCustom', 'Noto Sans', 'Noto Color Emoji', sans-serif !important;
        }
    `);

    // --- Compiled Regex and Character Replacements ---
    const persianRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
    const replacementRegex = /[\u064A\u0643]/g;

    const characterReplacements = {
        '\u064A': '\u06CC', // Arabic Yeh -> Persian Yeh
        '\u0643': '\u06A9'  // Arabic Kaf -> Persian Kaf
    };

    // --- Optimized Text Fixing ---
    const fixText = (text) => {
        if (!persianRegex.test(text)) return text;
        return text.replace(replacementRegex, char => characterReplacements[char]);
    };

    // --- High-Performance DOM Traversal ---
    const fixPersianCharsInNode = (rootNode) => {
        // Quick check: if the root element doesn't contain Persian text, skip entirely
        if (rootNode.nodeType === Node.ELEMENT_NODE && !persianRegex.test(rootNode.textContent)) {
            return;
        }

        const treeWalker = document.createTreeWalker(
            rootNode,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );

        let currentNode;
        while ((currentNode = treeWalker.nextNode())) {
            const parentElement = currentNode.parentElement;
            if (!parentElement) continue;

            const parentTag = parentElement.tagName;
            if (parentTag === 'SCRIPT' || parentTag === 'STYLE') continue;

            const originalValue = currentNode.nodeValue;
            if (!originalValue || !persianRegex.test(originalValue)) continue;

            const fixedValue = fixText(originalValue);
            if (originalValue !== fixedValue) {
                currentNode.nodeValue = fixedValue;
            }
        }
    };

    // --- Input and Textarea Handling ---
    const processInputElement = (el) => {
        if (el.dataset.__persianFixAttached) return;
        el.dataset.__persianFixAttached = "true";

        // Fix initial value if it contains Persian text
        if (el.value && persianRegex.test(el.value)) {
            el.value = fixText(el.value);
        }

        // Add input event listener for real-time fixing
        el.addEventListener('input', () => {
            const originalValue = el.value;
            if (!originalValue || !persianRegex.test(originalValue)) return;

            const fixedValue = fixText(originalValue);
            if (originalValue !== fixedValue) {
                const start = el.selectionStart;
                const end = el.selectionEnd;
                el.value = fixedValue;

                // Only restore selection if it was previously set
                if (start !== null && end !== null) {
                    el.setSelectionRange(start, end);
                }
            }
        });
    };

    const processAllInputs = (root = document) => {
        const elements = root.querySelectorAll('input[type="text"], input[type="search"], textarea');
        for (const el of elements) {
            processInputElement(el);
        }
    };

    // --- Optimized Mutation Observer ---
    let observerScheduled = false;
    const pendingMutations = new Set(); // Use Set to avoid duplicate processing

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            pendingMutations.add(mutation);
        }

        if (!observerScheduled) {
            observerScheduled = true;
            setTimeout(() => {
                const nodesToProcess = new Set();

                for (const mutation of pendingMutations) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
                            nodesToProcess.add(node);
                        }
                    }
                }

                pendingMutations.clear();

                for (const node of nodesToProcess) {
                    fixPersianCharsInNode(node);
                    processAllInputs(node);
                }

                observerScheduled = false;
            }, 200);
        }
    });

    // --- Script Initialization ---
    const start = () => {
        // Process existing content
        fixPersianCharsInNode(document.body);
        processAllInputs(document);

        // Start observing for dynamic content
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    };

    const waitForBody = () => {
        if (document.body) {
            start();
        } else {
            requestAnimationFrame(waitForBody);
        }
    };

    waitForBody();

})();

QingJ © 2025

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