Font Replacer

Replaces specified fonts with alternatives across all page elements

当前为 2025-03-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         Font Replacer
// @namespace    https://openuserjs.org/users/pfzim
// @version      0.2
// @description  Replaces specified fonts with alternatives across all page elements
// @author       pfzim
// @copyright    2025, pfzim (https://openuserjs.org/users/pfzim)
// @license      GPL-3.0-or-later
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function ()
{
	'use strict';

	// Font replacement settings (format: { "target font": "replacement", ... })
	const fontReplacements = {
		"Helvetica": "Verdana",
		"Kaspersky Sans": "Verdana",
		"Verdana Neue": "Verdana",
		"GitLab Sans": "Verdana",
		"Segoe UI": "Arial",
		"Inter": "Arial",
		"Georgia": "Times New Roman",
		"Roboto Mono": "Courier New",
		"Roboto": "Verdana",
		"Metropolis": "Verdana",
		"Open Sans": "Verdana",
		"Manrope": "Verdana",
		"GitLab Mono": "Courier New"
		//"Inter": "Arial"
		// Add your custom replacements here
	};

	function parseAndReplaceFonts(fontFamilyString, replacements)
	{
		if(!fontFamilyString) return '';

		const withoutComments = fontFamilyString.replace(/\/\*.*?\*\//g, '');
		const fontList = [];
		let currentFont = '';
		let inQuotes = false;
		let quoteChar = null;
		let inParentheses = false;
		let escapeNext = false;

		for(let i = 0; i < withoutComments.length; i++)
		{
			const char = withoutComments[i];

			if(escapeNext)
			{
				currentFont += char;
				escapeNext = false;
				continue;
			}

			if(char === '\\')
			{
				escapeNext = true;
				currentFont += char;
				continue;
			}

			if((char === '"' || char === "'") && !inParentheses)
			{
				if(!inQuotes)
				{
					inQuotes = true;
					quoteChar = char;
				} else if(char === quoteChar)
				{
					inQuotes = false;
					quoteChar = null;
				}
				currentFont += char;
			} else if(char === '(' && !inQuotes)
			{
				inParentheses = true;
				currentFont += char;
			} else if(char === ')' && !inQuotes)
			{
				inParentheses = false;
				currentFont += char;
			} else if(char === ',' && !inQuotes && !inParentheses)
			{
				if(currentFont)
					fontList.push(processFont(currentFont, replacements));
				currentFont = '';
			} else
			{
				currentFont += char;
			}
		}

		if(currentFont)
			fontList.push(processFont(currentFont, replacements));

		return fontList.join(', ');
	}

	function processFont(font, replacements)
	{
		let unquotedFont = font;

		font = font.trim();
		if(font.startsWith('"') && font.endsWith('"'))
		{
			unquotedFont = font.slice(1, -1).replace(/\\"/g, '"');
		}
		else if(font.startsWith("'") && font.endsWith("'"))
		{
			unquotedFont = font.slice(1, -1).replace(/\\'/g, "'");
		}

		const lowerFont = unquotedFont.toLowerCase();

		for(const [original, replacement] of Object.entries(replacements))
		{
			if(lowerFont === original.toLowerCase())
			{
				return replacement;
			}
		}

		return unquotedFont;
	}

	// // Function to replace fonts in a string
	// function replaceFonts(fontFamily)
	// {
	// 	let newFontFamily = fontFamily;

	// 	for(const [oldFont, newFont] of Object.entries(fontReplacements))
	// 	{
	// 		newFontFamily = newFontFamily.replace(
	// 			new RegExp(`\\b${oldFont}\\b`, 'gi'),
	// 			newFont
	// 		);
	// 		// Alternative matching approach (commented out):
	// 		// if(newFontFamily.toLowerCase().includes(oldFont.toLowerCase()))
	// 		// {
	// 		// 	return newFont;
	// 		// }
	// 	}

	// 	return newFontFamily;
	// }

	// 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, fontReplacements)

		if(newFont.toLowerCase() !== originalFont.toLowerCase())
		{
			element.style.fontFamily = newFont;
			// Debug logging (commented out):
			// console.log('Old font: ' + originalFont + '\nNew font: ' + newFont);
		}
	}

	// Recursive function to check all elements
	function checkAllElements(node)
	{
		processElement(node);

		for(let i = 0; i < node.children.length; i++)
		{
			checkAllElements(node.children[i]);
		}
	}

	// Process the entire page
	checkAllElements(document.body);

	// Monitor dynamically added elements
	const observer = new MutationObserver(mutations =>
	{
		mutations.forEach(mutation =>
		{
			mutation.addedNodes.forEach(node =>
			{
				if(node.nodeType === 1) // Node.ELEMENT_NODE
				{
					checkAllElements(node);
				}
			});
		});
	});

	observer.observe(document.body, {
		childList: true,
		subtree: true
	});

	// 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);

})();