Font Replacer

Replaces specified fonts with alternatives across all page elements

目前為 2025-03-28 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Font Replacer
  3. // @namespace https://openuserjs.org/users/pfzim
  4. // @version 0.2
  5. // @description Replaces specified fonts with alternatives across all page elements
  6. // @author pfzim
  7. // @copyright 2025, pfzim (https://openuserjs.org/users/pfzim)
  8. // @license GPL-3.0-or-later
  9. // @match *://*/*
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function ()
  14. {
  15. 'use strict';
  16.  
  17. // Font replacement settings (format: { "target font": "replacement", ... })
  18. const fontReplacements = {
  19. "Helvetica": "Verdana",
  20. "Kaspersky Sans": "Verdana",
  21. "Verdana Neue": "Verdana",
  22. "GitLab Sans": "Verdana",
  23. "Segoe UI": "Arial",
  24. "Inter": "Arial",
  25. "Georgia": "Times New Roman",
  26. "Roboto Mono": "Courier New",
  27. "Roboto": "Verdana",
  28. "Metropolis": "Verdana",
  29. "Open Sans": "Verdana",
  30. "Manrope": "Verdana",
  31. "GitLab Mono": "Courier New"
  32. //"Inter": "Arial"
  33. // Add your custom replacements here
  34. };
  35.  
  36. function parseAndReplaceFonts(fontFamilyString, replacements)
  37. {
  38. if(!fontFamilyString) return '';
  39.  
  40. const withoutComments = fontFamilyString.replace(/\/\*.*?\*\//g, '');
  41. const fontList = [];
  42. let currentFont = '';
  43. let inQuotes = false;
  44. let quoteChar = null;
  45. let inParentheses = false;
  46. let escapeNext = false;
  47.  
  48. for(let i = 0; i < withoutComments.length; i++)
  49. {
  50. const char = withoutComments[i];
  51.  
  52. if(escapeNext)
  53. {
  54. currentFont += char;
  55. escapeNext = false;
  56. continue;
  57. }
  58.  
  59. if(char === '\\')
  60. {
  61. escapeNext = true;
  62. currentFont += char;
  63. continue;
  64. }
  65.  
  66. if((char === '"' || char === "'") && !inParentheses)
  67. {
  68. if(!inQuotes)
  69. {
  70. inQuotes = true;
  71. quoteChar = char;
  72. } else if(char === quoteChar)
  73. {
  74. inQuotes = false;
  75. quoteChar = null;
  76. }
  77. currentFont += char;
  78. } else if(char === '(' && !inQuotes)
  79. {
  80. inParentheses = true;
  81. currentFont += char;
  82. } else if(char === ')' && !inQuotes)
  83. {
  84. inParentheses = false;
  85. currentFont += char;
  86. } else if(char === ',' && !inQuotes && !inParentheses)
  87. {
  88. if(currentFont)
  89. fontList.push(processFont(currentFont, replacements));
  90. currentFont = '';
  91. } else
  92. {
  93. currentFont += char;
  94. }
  95. }
  96.  
  97. if(currentFont)
  98. fontList.push(processFont(currentFont, replacements));
  99.  
  100. return fontList.join(', ');
  101. }
  102.  
  103. function processFont(font, replacements)
  104. {
  105. let unquotedFont = font;
  106.  
  107. font = font.trim();
  108. if(font.startsWith('"') && font.endsWith('"'))
  109. {
  110. unquotedFont = font.slice(1, -1).replace(/\\"/g, '"');
  111. }
  112. else if(font.startsWith("'") && font.endsWith("'"))
  113. {
  114. unquotedFont = font.slice(1, -1).replace(/\\'/g, "'");
  115. }
  116.  
  117. const lowerFont = unquotedFont.toLowerCase();
  118.  
  119. for(const [original, replacement] of Object.entries(replacements))
  120. {
  121. if(lowerFont === original.toLowerCase())
  122. {
  123. return replacement;
  124. }
  125. }
  126.  
  127. return unquotedFont;
  128. }
  129.  
  130. // // Function to replace fonts in a string
  131. // function replaceFonts(fontFamily)
  132. // {
  133. // let newFontFamily = fontFamily;
  134.  
  135. // for(const [oldFont, newFont] of Object.entries(fontReplacements))
  136. // {
  137. // newFontFamily = newFontFamily.replace(
  138. // new RegExp(`\\b${oldFont}\\b`, 'gi'),
  139. // newFont
  140. // );
  141. // // Alternative matching approach (commented out):
  142. // // if(newFontFamily.toLowerCase().includes(oldFont.toLowerCase()))
  143. // // {
  144. // // return newFont;
  145. // // }
  146. // }
  147.  
  148. // return newFontFamily;
  149. // }
  150.  
  151. // Main element processing function
  152. function processElement(element)
  153. {
  154. const computedStyle = window.getComputedStyle(element);
  155. const originalFont = computedStyle.fontFamily;
  156.  
  157. if(!originalFont) return;
  158.  
  159. //const newFont = replaceFonts(originalFont);
  160. const newFont = parseAndReplaceFonts(originalFont, fontReplacements)
  161.  
  162. if(newFont.toLowerCase() !== originalFont.toLowerCase())
  163. {
  164. element.style.fontFamily = newFont;
  165. // Debug logging (commented out):
  166. // console.log('Old font: ' + originalFont + '\nNew font: ' + newFont);
  167. }
  168. }
  169.  
  170. // Recursive function to check all elements
  171. function checkAllElements(node)
  172. {
  173. processElement(node);
  174.  
  175. for(let i = 0; i < node.children.length; i++)
  176. {
  177. checkAllElements(node.children[i]);
  178. }
  179. }
  180.  
  181. // Process the entire page
  182. checkAllElements(document.body);
  183.  
  184. // Monitor dynamically added elements
  185. const observer = new MutationObserver(mutations =>
  186. {
  187. mutations.forEach(mutation =>
  188. {
  189. mutation.addedNodes.forEach(node =>
  190. {
  191. if(node.nodeType === 1) // Node.ELEMENT_NODE
  192. {
  193. checkAllElements(node);
  194. }
  195. });
  196. });
  197. });
  198.  
  199. observer.observe(document.body, {
  200. childList: true,
  201. subtree: true
  202. });
  203.  
  204. // Optional: Add @font-face style to force font replacement (commented out)
  205. // const style = document.createElement('style');
  206. // style.textContent = `
  207. // * {
  208. // font-family: ${Object.values(fontReplacements).join(', ')} !important;
  209. // }
  210. // `;
  211. // document.head.appendChild(style);
  212.  
  213. })();