Apply Vazir font to Persian/RTL content across selected websites
当前为
// ==UserScript==
// @name Persian Font Fix (Vazir)
// @namespace https://greasyfork.org/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();
})();