NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Font Replacer // @version 0.3 // @description Replaces specified fonts with alternatives across all page elements // @author pfzim // @copyright 2025, pfzim (https://github.com/pfzim/font-replacer) // @license GPL-3.0-or-later // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; // Font replacement settings (format: { "target font": "replacement", ... }) let fontConfig = [ { "pattern_url": "^http[s]?://[^/]*gitlab\\.com/", "replacements": { "GitLab": "Verdana", "GitLab Sans": "Verdana", "GitLab Mono": "Courier New" } }, { "pattern_url": "^http[s]?://[^/]*market\\.yandex\\.ru/", "replacements": { "YS Text": "Arial" }, "skip_body": true, "skip_observer": true, "skip_observer_css": false, "skip_styles": false, "delay_ms": 0, "debug": false }, { "pattern_url": ".*", "replacements": { "Barlow": "Verdana", "Geist": "Verdana", "Geist Mono": "Courier New", "Georgia": "Times New Roman", "GitLab Mono": "Courier New", "GitLab Sans": "Verdana", "Golos Text": "Arial", "Golos": "Arial", "Google Sans": "Verdana", "GothamProRegular": "Verdana", "Helvetica": "Verdana", "Inter": "Arial", "Kaspersky Sans": "Verdana", "Lato": "Arial", "Lato": "Verdana", "Manrope": "Verdana", "Metropolis": "Verdana", "Museo Sans": "Verdana", "Open Sans": "Verdana", "Optimistic Display": "Verdana", "Optimistic Text": "Verdana", "Roboto Mono": "Courier New", "Roboto": "Verdana", "Segoe UI": "Arial", "Source Code Pro": "Courier New", "Stolzl": "Verdana", "Verdana Neue": "Verdana", "ui-sans-serif": "Arial" }, "skip_body": false, "skip_observer": false, "skip_styles": false, "skip_observer_css": false, "skip_styles": false, "delay_ms": 0, "debug": false } // Add your custom replacements here ]; let replacement_rule = null; let sheet_count = 0; let debug = false; replacement_rule = getReplacementsForCurrentSite(); if (replacement_rule && Object.keys(replacement_rule.replacements).length > 0) { if (replacement_rule.debug) { debug = true; console.log('Font Replacer rules:', replacement_rule); } // Process the entire page setTimeout(() => { if (!replacement_rule.skip_styles) processAllStyles(document.styleSheets); if (!replacement_rule.skip_observer_css) startObserverCSS(); if (!replacement_rule.skip_body) processAllElements(document.body); if (!replacement_rule.skip_observer) startObserver(); }, replacement_rule.delay_ms || 0); } else { console.log('Font Replacer: disabled for this url or globally!'); } function startObserver() { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // Node.ELEMENT_NODE processAllElements(node); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function startObserverCSS() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === 'STYLE') { if (debug) console.log('Added node <style>:', node); // setTimeout(() => { processSheetStyles(node.sheet); // }, 1000); } else if (node.nodeName === 'LINK' && node.rel === 'stylesheet') { if (debug) console.log('Added CSS file:', node); node.addEventListener('load', () => { try { if (debug) console.log('CSS file loaded'); processSheetStyles(node.sheet); } catch (e) { console.warn('Failed access to CSS rules (CORS):', e); } }); } }); }); }); observer.observe(document.head, { childList: true, subtree: true }); } function getReplacementsForCurrentSite() { const url = window.location.href; for (const rule of fontConfig) { try { const regex = new RegExp(rule.pattern_url); if (regex.test(url)) { console.log('Font Replacer: matched pattern: ' + rule.pattern_url); return rule || {}; } } catch (e) { console.warn(`Invalid regex pattern: ${rule.pattern_url}`, e); } } return null; } function parseAndReplaceFonts(fontFamilyString, replacements) { const regex = new RegExp(/^(\s*var\s*\()(.*)(\)\s*)$/i); const replaceFont = (fontStr) => { // console.log('replaceFont: ' + fontStr); const matches = fontStr.match(regex) if (matches) { return matches[1] + parse(matches[2]) + matches[3]; } else { let unquotedFont = fontStr; unquotedFont = unquotedFont.trim(); if (unquotedFont.startsWith('"') && unquotedFont.endsWith('"')) { unquotedFont = unquotedFont.slice(1, -1).replace(/\\"/g, '"'); } else if (unquotedFont.startsWith("'") && unquotedFont.endsWith("'")) { unquotedFont = unquotedFont.slice(1, -1).replace(/\\'/g, "'"); } const lowerFont = unquotedFont.toLowerCase(); for (const [original, replacement] of Object.entries(replacements)) { if (lowerFont === original.toLowerCase()) { return replacement; } } } return fontStr; }; const parse = (str) => { // console.log('parse: ' + str); let result = ''; let current = ''; let inQuotes = false; let inFunction = 0; let quoteChar = null; for (let i = 0; i < str.length; i++) { let ch = str[i]; if (!inQuotes) { if ((ch === '"' || ch === "'")) { inQuotes = true; quoteChar = ch; } else if (ch === '(') { inFunction++; } else if (ch === ')' && inFunction > 0) { inFunction--; } else if (ch === ',' && inFunction === 0) { const processed = replaceFont(current); result += processed + ch; current = ''; continue; } } else { if (ch === quoteChar) { inQuotes = false; quoteChar = null; } else if (ch === '\\') { current += ch; i++; ch = str[i]; } } current += ch; } if (current) { result += replaceFont(current); } return result; }; return parse(fontFamilyString); } // Main element processing function function processElement(element) { const computedStyle = window.getComputedStyle(element); const originalFont = computedStyle.fontFamily; if (!originalFont) return; //const newFont = replaceFonts(originalFont); const newFont = parseAndReplaceFonts(originalFont, replacement_rule.replacements) if (newFont.toLowerCase() !== originalFont.toLowerCase()) { element.style.fontFamily = newFont; // Debug logging (commented out): if (debug) console.log('Old font: ' + originalFont + '\nNew font: ' + newFont); } } // Recursive function to check all elements function processAllElements(node) { processElement(node); for (let i = 0; i < node.children.length; i++) { processAllElements(node.children[i]); } } function processSheetStyles(sheet) { try { if (debug) { console.log('Processing CSS node:', sheet.ownerNode); if (!sheet.cssRules) { console.log('CSS rules not accessible - possible CORS issue'); } if (sheet.disabled) { console.log('Stylesheet is currently disabled'); } } Array.from(sheet.cssRules || []).forEach(rule => { if ((rule instanceof CSSStyleRule) && rule.style) { // not rule instanceof CSSFontFaceRule //console.log('Rule:'); // Доступ к свойствам: //console.log('Selector:', rule.selectorText); for (let k = rule.style.length; k--;) { const var_name = rule.style[k]; if (var_name.startsWith('--')) { const originalFont = rule.style.getPropertyValue(var_name).trim(); const newFont = parseAndReplaceFonts(originalFont, replacement_rule.replacements) if (newFont.toLowerCase() !== originalFont.toLowerCase()) { rule.style.setProperty(var_name, newFont, rule.style.getPropertyPriority(var_name)); // Debug logging (commented out): if (debug) console.log('Var: ' + var_name + '\nOld font: ' + originalFont + '\nNew font: ' + newFont); } } } if (rule.style.fontFamily) { // not rule instanceof CSSFontFaceRule // Removes the !important //rule.style.fontFamily = rule.style.fontFamily; // if(rule.style.getPropertyPriority('font-family') === 'important') // rule.style.setProperty('font-family', rule.style.getPropertyValue('font-family'), null); // Replace fonts const originalFont = rule.style.getPropertyValue('font-family').trim(); const newFont = parseAndReplaceFonts(originalFont, replacement_rule.replacements) if (newFont.toLowerCase() !== originalFont.toLowerCase()) { rule.style.setProperty('font-family', newFont, rule.style.getPropertyPriority('font-family')); // Debug logging (commented out): if (debug) console.log('Old font: ' + originalFont + '\nNew font: ' + newFont); } } } }); sheet_count++; if (debug) console.log('Font Replacer: CSS sheets processed: ' + sheet_count + ' (+1)'); } catch (e) { console.warn('Font Replacer: Failed access to CSS rules (CORS)', e); //console.log('sheet.ownerNode.textContent:', sheet.ownerNode); } } // Recursive function to check all styles function processAllStyles(node) { if (debug) console.log('Font Replacer: Process all styles...'); Array.from(node).forEach(sheet => { processSheetStyles(sheet); }); } // Optional: Add @font-face style to force font replacement (commented out) // const style = document.createElement('style'); // style.textContent = ` // * { // font-family: ${Object.values(fontReplacements).join(', ')} !important; // } // `; // document.head.appendChild(style); })();