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