Font Replacer

Replaces specified fonts with alternatives across all page elements

// ==UserScript==
// @name         Font Replacer
// @version      0.4
// @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
// @namespace https://openuserjs.org/users/pfzim
// ==/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,
            "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);

})();

QingJ © 2025

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