Greasy Fork is available in English.
将网页字体替换为 MiSans,资源使用外部注入
// ==UserScript==
// @name MiSans 字体网页替换脚本
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 将网页字体替换为 MiSans,资源使用外部注入
// @author Wolfe
// @match *://*/*
// @exclude *://h.bkzx.cn/*
// @run-at document-start
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ======================
// 1. 核心配置
// ======================
const CONFIG = {
BASE_URL: 'https://cdn.jsdelivr.net/npm/[email protected]/lib',
EMOJI_URL: 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap',
NOTO_URL: 'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:[email protected]&family=Noto+Sans+TC:[email protected]&family=Noto+Sans+JP:[email protected]&family=Noto+Sans+KR:[email protected]&display=swap',
DEBUG: false
};
// ======================
// 1.1 字体栈定义 (已优化:移除本地系统字体)
// ======================
// [优化] 仅使用通用 monospace,移除 Consolas/Menlo 等本地字体,避免系统字体查找开销
const MONO_STACK = `monospace`;
// [优化] 仅使用 Web Emoji
const EMOJI_STACK = `"Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji"`;
// 小语种优先栈
const MINOR_LANG_STACK = `
"MiSans Arabic", "MiSans Thai", "MiSans Tibetan", "MiSans Myanmar",
"MiSans Lao", "MiSans Khmer", "MiSans Gurmukhi", "MiSans Devanagari"
`.replace(/\s+/g, ' ').trim();
// [关键] 思源 CJK 兜底栈:移除 "Source Han Sans" 本地调用,强制使用 Web 版 Noto
const NOTO_FALLBACK_STACK = `
"Noto Sans SC", "Noto Sans TC", "Noto Sans JP", "Noto Sans KR",
"Noto Sans CJK SC"
`.replace(/\s+/g, ' ').trim();
// ======================
// 2. 排除规则
// ======================
const EXCLUSIONS = {
TAGS: new Set([
'style', 'noscript', 'svg', 'path', 'rect', 'circle', 'line', 'polyline', 'polygon',
'img', 'canvas', 'video', 'audio', 'i', 'math', 'base', 'template', 'track', 'source', 'em',
'code', 'pre', 'kbd', 'samp', 'tt', 'var', 'cds', 'xmp', 'script', 'meta', 'link', 'i', 'a'
]),
// 保持原有的选择器列表
SELECTORS: [
'.material-symbols-outlined', '.material-icons', '.material-icons-outlined',
'.fa', '.fas', '.far', '.fal', '.fab', '.fad',
'.glyphicon', '.icon', '.icons', '.i',
'[class*="ms-Icon"]', '[class*="Fabric"]', '[class*="fui-Icon"]',
'[class*="icon-"]', '[class*="ico-"]', '[class*="ri-"]', '[class*="nf-"]',
'.ms-Button-icon', '[role="img"]', '.octicon',
'[class*="keyword"]', '[class*="hljs"]', '.token',
'.katex', '.katex *', '.MathJax', '.MathJax *', '.mjx-container', '.mjx-math',
'.math', '.latex', '.tex', '.notion-equation-inline', '.notion-equation-block',
'.blob-code-inner', '.text-mono', '.SFMono-Regular',
'.code-block', '.highlight', '.syntaxhighlighter', '[class*="monospace"]',
'.monaco-editor', '.CodeMirror', '.cm-content', '[class*="ace"]', '[class*="symbols"]',
'.docon', '[class*="icon"]', '[class*="video"]', '[class*="player"]', '[class*="svg"]',
'[class*="Button"]'
]
};
const CSS_VARIABLES_TO_HIJACK = [
'--font-family', '--font-sans', '--font-serif',
'--font-body', '--font-heading', '--font-display', '--font-base',
'--font-primary', '--font-secondary',
'--bs-body-font-family', '--bs-font-sans-serif',
'--chakra-fonts-body', '--chakra-fonts-heading',
'--antd-font-family',
'--mdc-typography-font-family', '--mat-typography-font-family',
'--el-font-family',
'--font-sans-serif',
'--fontStack-system', '--fontStack-sansSerif',
'--system-ui', '--ui-font'
];
// ======================
// 3. 字重映射策略
// ======================
const WEIGHT_MAP = {
'Thin': 100, 'ExtraLight': 200, 'Light': 300, 'Regular': 400,
'Medium': 500, 'Demibold': 600, 'Bold': 700, 'Heavy': 900
};
const LATIN_BOOST_MAP = {
'Thin': 'Regular', 'ExtraLight': 'Medium', 'Light': 'Medium',
'Regular': 'Demibold', 'Medium': 'Bold', 'Demibold': 'Heavy', 'Bold': 'Heavy', 'Heavy': 'Heavy'
};
const STANDARD_MAP = {
'Thin': 'Thin', 'ExtraLight': 'ExtraLight', 'Light': 'Light', 'Regular': 'Regular',
'Medium': 'Medium', 'Demibold': 'Demibold', 'Bold': 'Bold', 'Heavy': 'Heavy'
};
// ======================
// 4. 变体配置
// ======================
const VARIANTS = {
'Latin': { dir: 'Latin', prefix: 'MiSansLatin', type: 'woff2', map: LATIN_BOOST_MAP, name: 'MiSans Latin' },
'Normal': { dir: 'Normal', prefix: 'MiSans', type: 'css', map: STANDARD_MAP, name: 'MiSans' },
'TC': { dir: 'TC', prefix: 'MisansTC', type: 'css', map: STANDARD_MAP, name: 'MiSans TC' },
'Arabic': { dir: 'Arabic', prefix: 'MiSansArabic', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Arabic' },
'Thai': { dir: 'Thai', prefix: 'MiSansThai', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Thai' },
'Tibetan': { dir: 'Tibetan', prefix: 'MiSansTibetan', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Tibetan' },
'Myanmar': { dir: 'Myanmar', prefix: 'MiSansMyanmar', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Myanmar' },
'Lao': { dir: 'Lao', prefix: 'MiSansLao', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Lao' },
'Khmer': { dir: 'Khmer', prefix: 'MiSansKhmer', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Khmer' },
'Gurmukhi': { dir: 'Gurmukhi', prefix: 'MiSansGurmukhi', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Gurmukhi' },
'Devanagari': { dir: 'Devanagari', prefix: 'MiSansDevanagari', type: 'woff2', map: STANDARD_MAP, name: 'MiSans Devanagari' }
};
// ======================
// 5. 字体栈构建
// ======================
const BASE_LATIN = `"MiSans Latin"`;
const TC_FALLBACKS = `"MiSans TC", "MiSansTC", "Misans TC"`;
// 全局兜底 (无本地字体)
const GLOBAL_FALLBACKS = `${MINOR_LANG_STACK}, "MiSans", "MiSans Normal", ${TC_FALLBACKS}, ${NOTO_FALLBACK_STACK}, ${EMOJI_STACK}`;
// 专用栈 (移除本地 Source Han Sans)
const KR_PRIORITY_STACK = `${BASE_LATIN}, "Noto Sans KR", "MiSans", ${GLOBAL_FALLBACKS}`;
const JP_PRIORITY_STACK = `${BASE_LATIN}, "Noto Sans JP", "MiSans", ${GLOBAL_FALLBACKS}`;
function buildStack(primaryFont) {
if (primaryFont === "MiSans TC") return `${BASE_LATIN}, ${TC_FALLBACKS}, "MiSans", ${GLOBAL_FALLBACKS}`;
if (primaryFont === "MiSans") return `${BASE_LATIN}, ${GLOBAL_FALLBACKS}`;
return `${BASE_LATIN}, "${primaryFont}", ${GLOBAL_FALLBACKS}`;
}
const STACKS = {
sc: buildStack("MiSans"),
tc: buildStack("MiSans TC"),
kr: KR_PRIORITY_STACK,
ja: JP_PRIORITY_STACK,
ar: buildStack("MiSans Arabic"),
th: buildStack("MiSans Thai"),
bo: buildStack("MiSans Tibetan"),
my: buildStack("MiSans Myanmar"),
lo: buildStack("MiSans Lao"),
km: buildStack("MiSans Khmer"),
pa: buildStack("MiSans Gurmukhi"),
hi: buildStack("MiSans Devanagari")
};
// ======================
// 6. 语言检测 (保留完整功能)
// ======================
const REGEX = {
TC: /[\u4E26\u50B3\u5169\u5340\u53C3\u570B\u5BE6\u5BEB\u5C0D\u5F8C\u61C9\u6230\u6416\u64D4\u64F4\u65BC\u6703\u689D\u6A02\u6A23\u6B77\u6B78\u6EFE\u6FDF\u7063\u70BA\u723E\u73FE\u7522\u7BC4\u7D00\u7D44\u7D93\u7E7C\u7E8C\u806F\u807D\u81FA\u8207\u840A\u862D\u88FD\u8A71\u8A72\u8AAA\u8B5C\u8B8A\u8C9D\u8CB7\u8CD3\u8CE3\u9019\u904E\u9054\u9084\u908A\u968A\u985E\u99AC\u9AD4\u9EBC\u9EDE]/,
TH: /[\u0E00-\u0E7F]/,
AR: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/,
BO: /[\u0F00-\u0FFF]/,
MY: /[\u1000-\u109F]/,
LO: /[\u0E80-\u0EFF]/,
KM: /[\u1780-\u17FF]/,
PA: /[\u0A00-\u0A7F]/,
HI: /[\u0900-\u097F]/
};
function detectLanguage(element) {
// 1. 属性检测
const langAttr = element.closest('[lang]')?.lang?.toLowerCase();
if (langAttr) {
if (langAttr.includes('zh-tw') || langAttr.includes('zh-hk') || langAttr.includes('hant')) return 'tc';
if (langAttr.includes('ja')) return 'ja';
if (langAttr.includes('ko') || langAttr.includes('kr')) return 'kr';
if (langAttr.includes('ar') || langAttr.includes('ur') || langAttr.includes('fa')) return 'ar';
if (langAttr.includes('th')) return 'th';
if (langAttr.includes('bo')) return 'bo';
if (langAttr.includes('my')) return 'my';
if (langAttr.includes('lo')) return 'lo';
if (langAttr.includes('km')) return 'km';
if (langAttr.includes('pa')) return 'pa';
if (langAttr.includes('hi')) return 'hi';
}
// 2. 文本内容检测 (优化:仅检测有文本内容的元素)
// 使用 textContent 可能会引起回流,但为了准确性必须保留,通过限制长度优化
const text = element.textContent;
if (text && text.length > 0) {
const sample = text.substring(0, 50); // [优化] 减少采样长度,从300减至50,足够判断
if (REGEX.TC.test(sample)) return 'tc';
if (REGEX.AR.test(sample)) return 'ar';
if (REGEX.TH.test(sample)) return 'th';
if (REGEX.BO.test(sample)) return 'bo';
if (REGEX.MY.test(sample)) return 'my';
if (REGEX.LO.test(sample)) return 'lo';
if (REGEX.KM.test(sample)) return 'km';
if (REGEX.PA.test(sample)) return 'pa';
if (REGEX.HI.test(sample)) return 'hi';
}
return 'sc';
}
// ======================
// 7. FontLoader (优化:增加 preconnect)
// ======================
class FontLoader {
constructor() {
this.loaded = false;
}
loadFonts() {
if (this.loaded) return;
const head = document.head || document.documentElement;
// [优化] 预连接加速
['https://cdn.jsdelivr.net', 'https://fonts.googleapis.com', 'https://fonts.gstatic.com'].forEach(href => {
const link = document.createElement('link');
link.rel = 'preconnect'; link.href = href;
if (href.includes('gstatic')) link.crossOrigin = 'anonymous';
head.appendChild(link);
});
const cssLinksContainer = document.createDocumentFragment();
const style = document.createElement('style');
style.id = 'nuclear-font-loader';
let cssContent = '/* --- MiSans Hybrid v55 (Optimized) --- */\n';
const emojiLink = document.createElement('link');
emojiLink.rel = 'stylesheet'; emojiLink.href = CONFIG.EMOJI_URL; emojiLink.crossOrigin = 'anonymous';
cssLinksContainer.appendChild(emojiLink);
const notoLink = document.createElement('link');
notoLink.rel = 'stylesheet'; notoLink.href = CONFIG.NOTO_URL; notoLink.crossOrigin = 'anonymous';
cssLinksContainer.appendChild(notoLink);
Object.keys(VARIANTS).forEach(key => {
const conf = VARIANTS[key];
const mapping = conf.map;
if (conf.type === 'woff2') {
Object.keys(WEIGHT_MAP).forEach(cssWeightName => {
const cssWeightValue = WEIGHT_MAP[cssWeightName];
const actualFileWeightName = mapping[cssWeightName];
if (actualFileWeightName) {
const url = `${CONFIG.BASE_URL}/${conf.dir}/${conf.prefix}-${actualFileWeightName}.woff2`;
cssContent += `@font-face { font-family: '${conf.name}'; src: url('${url}') format('woff2'); font-weight: ${cssWeightValue}; font-style: normal; font-display: swap; }\n`;
}
});
}
else if (conf.type === 'css') {
const weightsToLoad = new Set(Object.values(mapping));
weightsToLoad.forEach(weightName => {
const url = `${CONFIG.BASE_URL}/${conf.dir}/${conf.prefix}-${weightName}.min.css`;
const link = document.createElement('link');
link.rel = 'stylesheet'; link.href = url; link.crossOrigin = 'anonymous';
cssLinksContainer.appendChild(link);
});
}
});
// [优化] 预编译选择器字符串
const tagExclusionSelector = Array.from(EXCLUSIONS.TAGS).map(tag => `:not(${tag})`).join('');
const customExclusionSelector = EXCLUSIONS.SELECTORS.map(sel => `:not(${sel})`).join('');
cssContent += `
:root, :host, body, html {
--font-full-sc: ${STACKS.sc};
--font-full-tc: ${STACKS.tc};
${CSS_VARIABLES_TO_HIJACK.map(v => `${v}: ${STACKS.sc} !important;`).join('\n ')}
--font-mono: ${MONO_STACK} !important;
--font-monospace: ${MONO_STACK} !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
text-rendering: optimizeLegibility !important;
}
a, *${tagExclusionSelector}${customExclusionSelector} {
font-family: ${STACKS.sc} !important;
}
[lang^="ja"] { font-family: ${STACKS.ja} !important; }
[lang^="ko"], [lang="kr"] { font-family: ${STACKS.kr} !important; }
[lang^="zh-TW"], [lang^="zh-HK"], [lang="zh-Hant"] { font-family: ${STACKS.tc} !important; }
[lang^="ar"], [lang^="fa"], [lang^="ur"] { font-family: ${STACKS.ar} !important; }
[lang^="th"] { font-family: ${STACKS.th} !important; }
`;
style.textContent = cssContent;
head.appendChild(cssLinksContainer);
head.appendChild(style);
this.loaded = true;
if (CONFIG.DEBUG) console.log('[Font Engine] Loaded. Hybrid v55 Optimized.');
}
}
// ======================
// 8. 异步处理系统 (优化逻辑)
// ======================
const processed = new WeakSet();
const updateQueue = new Map();
let isBatchScheduled = false;
const requestIdleCallback = window.requestIdleCallback || function(cb) {
return setTimeout(() => { cb({ timeRemaining: () => 50, didTimeout: false }); }, 1);
};
function scheduleBatchUpdate() {
if (isBatchScheduled) return;
isBatchScheduled = true;
requestIdleCallback(processBatchQueue, { timeout: 1000 });
}
function processBatchQueue(deadline) {
isBatchScheduled = false;
const iterator = updateQueue.entries();
let entry = iterator.next();
while (!entry.done) {
if (deadline.timeRemaining() < 1 && updateQueue.size > 0) {
scheduleBatchUpdate();
break;
}
const [el, forceUpdate] = entry.value;
updateQueue.delete(el);
if (el.isConnected) {
performProcess(el, forceUpdate);
}
entry = iterator.next();
}
}
function addToQueue(node, force = false) {
if (!node || node.nodeType !== 1) return;
// [优化] 快速过滤:如果已经在队列中且非强制,跳过
if (updateQueue.has(node) && !force) return;
updateQueue.set(node, force);
scheduleBatchUpdate();
}
function performProcess(el, forceUpdate = false) {
if (processed.has(el) && !forceUpdate) return;
// [优化] 快速标签检查
const tag = el.tagName.toLowerCase();
if (EXCLUSIONS.TAGS.has(tag)) {
processed.add(el);
return;
}
// [优化] 性能瓶颈优化:matches 检查较慢,先检查是否有 class 属性
if (el.className && typeof el.className === 'string') {
if (EXCLUSIONS.SELECTORS.some(selector => {
// 简单的字符串包含检查比 matches 快,但只适用于类名包含的情况
if (selector.startsWith('.') && el.className.includes(selector.substring(1))) return true;
try { return el.matches(selector); } catch (e) { return false; }
})) {
processed.add(el);
return;
}
}
// [优化] 关键性能提升:如果元素没有文本内容且没有子元素,或者是纯容器,先不进行昂贵的正则检测
// 只有当元素可能是“叶子节点”或者包含直接文本时才检测
if (!el.firstChild) {
processed.add(el);
return;
}
const lang = detectLanguage(el);
// 只有当检测出的语言不是默认 SC 时,才需要 JS 干预样式
// 因为 CSS 全局样式已经覆盖了 SC 的情况
if (lang !== 'sc') {
const targetFontStack = STACKS[lang];
if (targetFontStack) {
const currentStyle = el.style.fontFamily; // 直接读取 style 属性而不是 getComputedStyle,性能更高
if (forceUpdate || !currentStyle.includes('MiSans')) {
el.style.setProperty('font-family', targetFontStack, 'important');
}
}
}
processed.add(el);
}
// ======================
// 9. 观察者与初始化 (保留)
// ======================
function processNode(node) {
if (node.nodeType !== 1) return;
// [优化] 只有当节点包含文本或可能是文本容器时才加入队列
addToQueue(node);
if (node.shadowRoot) {
injectStylesIntoShadowRoot(node.shadowRoot);
// ShadowRoot 内部通常节点较少,可以直接遍历
node.shadowRoot.querySelectorAll('*').forEach(el => addToQueue(el));
}
// [优化] 避免 querySelectorAll('*') 的巨大开销
// 仅在初始化或大块插入时使用,平时依赖 MutationObserver 的 childList
if (node.childElementCount > 0) {
const descendants = node.getElementsByTagName('*'); // getElementsByTagName 比 querySelectorAll 快
for (let i = 0; i < descendants.length; i++) {
addToQueue(descendants[i]);
}
}
}
function injectStylesIntoShadowRoot(shadowRoot) {
if (!shadowRoot.getElementById('nuclear-font-loader')) {
const style = document.getElementById('nuclear-font-loader');
if (style) {
try { shadowRoot.appendChild(style.cloneNode(true)); } catch (e) {}
}
}
}
function processIframe(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc && !doc.__fontPatched) {
doc.__fontPatched = true;
const style = document.getElementById('nuclear-font-loader');
if (style && doc.head) doc.head.appendChild(style.cloneNode(true));
// Iframe 内部也使用优化后的遍历
const allNodes = doc.getElementsByTagName('*');
for (let i = 0; i < allNodes.length; i++) {
addToQueue(allNodes[i]);
}
const iframeObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => processNode(node));
} else if (mutation.type === 'attributes') {
addToQueue(mutation.target, true);
}
}
});
iframeObserver.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'lang'] });
}
} catch (e) {}
}
function setupObserver() {
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.tagName === 'IFRAME') {
node.addEventListener('load', () => processIframe(node), { once: true });
} else {
processNode(node);
}
}
} else if (mutation.type === 'attributes') {
// [优化] 仅当 style 或 lang 改变时才触发,减少 class 变更带来的抖动
const el = mutation.target;
if (processed.has(el)) {
const fam = el.style.fontFamily;
// 只有当字体不再是 MiSans 时才重新处理,防止死循环
if (fam && !fam.includes('MiSans')) {
addToQueue(el, true);
}
}
}
}
});
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'lang'] }); // 移除 class 监听以提升性能
}
const fontLoader = new FontLoader();
function init() {
fontLoader.loadFonts();
processNode(document.documentElement);
document.querySelectorAll('iframe').forEach(processIframe);
setupObserver();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();