Greasy Fork

Greasy Fork is available in English.

MiSans 字体网页替换脚本

将网页字体替换为 MiSans,资源使用外部注入

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();