// ==UserScript==
// @name Persian Font Fix (Vazir)
// @namespace https://github.com/sinazadeh/userscripts
// @version 2.1.0
// @description Improves the readability of Persian and RTL content by applying the Vazir font across supported 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/*
// @exclude *://*.google.*/recaptcha/*
// @grant GM_addStyle
// @run-at document-start
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/538095/Persian%20Font%20Fix%20%28Vazir%29.user.js
// @updateURL https://update.greasyfork.icu/scripts/538095/Persian%20Font%20Fix%20%28Vazir%29.meta.js
// ==/UserScript==
/* jshint esversion: 6 */
/* global requestIdleCallback */
(function () {
'use strict';
// --- 0. Inject font regardless of performance tweaks ---
GM_addStyle(`
@font-face {
font-family: 'VazirmatnFixed';
src: local('Vazirmatn');
font-display: swap;
unicode-range:
U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
}
body,
p,
div,
h1,
h2,
h3,
h4,
h5,
h6,
a,
li,
td,
th,
input[type='text'],
input[type='search'],
textarea,
select,
option,
label,
button,
blockquote,
summary,
details,
figcaption,
strong,
em,
span[lang^='fa'],
span[lang^='ar'],
span[dir='rtl'] {
font-family:
'VazirmatnFixed', 'Noto Sans', 'Apple Color Emoji', 'Noto Color Emoji',
'Twemoji Mozilla', 'Google Sans', 'Helvetica Neue', sans-serif !important;
}
html {
font-size: 16px;
}
`);
// --- 1. Only look for the two characters we actually replace ---
const replacementRegex = /[يك]/g;
const charMap = new Map([
['ي', 'ی'],
['ك', 'ک'],
]);
const fixText = text =>
text.replace(replacementRegex, c => charMap.get(c) || c);
// --- 2. Fast node‐by‐node replacement, only when needed ---
const processed = new WeakSet();
const walkerFilter = {
acceptNode(node) {
// only walk TEXT nodes that contain at least one replaceable char
return replacementRegex.test(node.nodeValue)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
},
};
function fixNode(root) {
if (processed.has(root) || !replacementRegex.test(root.textContent))
return;
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
walkerFilter,
false,
);
let node,
changed = false;
while ((node = walker.nextNode())) {
const orig = node.nodeValue;
const upd = fixText(orig);
if (orig !== upd) {
node.nodeValue = upd;
changed = true;
}
}
if (changed) processed.add(root);
}
// --- 3. Input elements: per-element debounce, no full re-scans ---
function attachInput(el) {
if (el.dataset.pfixAttached) return;
el.dataset.pfixAttached = '1';
const doFix = () => {
if (!replacementRegex.test(el.value)) return;
const orig = el.value;
const upd = fixText(orig);
if (orig === upd) return;
const start = el.selectionStart;
const end = el.selectionEnd;
el.value = upd;
if (start != null && end != null) {
try {
el.setSelectionRange(start, end);
} catch (err) {
// Ignore
}
}
};
let to;
el.addEventListener('input', () => {
clearTimeout(to);
to = setTimeout(doFix, 50);
});
// Initial fix
doFix();
}
// --- 4. Throttled, targeted MutationObserver ---
let pending = new Set(),
ticking = false;
function schedule() {
if (ticking) return;
ticking = true;
// run on idle if available
const exec = () => {
pending.forEach(node => {
if (
node.nodeType === Node.TEXT_NODE ||
node.nodeType === Node.ELEMENT_NODE
)
fixNode(node.nodeType === 1 ? node : node.parentElement);
});
pending.clear();
ticking = false;
};
if ('requestIdleCallback' in window)
requestIdleCallback(exec, {
timeout: 200,
});
else setTimeout(exec, 100);
}
const obs = new MutationObserver(muts => {
muts.forEach(m => {
if (
m.type === 'characterData' &&
replacementRegex.test(m.target.nodeValue)
) {
pending.add(m.target);
}
if (m.type === 'childList') {
m.addedNodes.forEach(n => {
if (n.nodeType === 3) {
// text node
if (replacementRegex.test(n.nodeValue)) pending.add(n);
} else if (n.nodeType === 1) {
// element
// if it has replaceable text somewhere in subtree
if (replacementRegex.test(n.textContent))
pending.add(n);
// if it’s an or