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