Font Rendering Lite (Core + Site Policy + Default Excludes)

极简字体美化:统一正文字体/等宽字体、可选平滑与字号缩放;全局与站点三态策略(启用/禁用/跟随);默认内置常见排除;带轻量设置面板;不改外链CSS以降低副作用

当前为 2025-09-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         Font Rendering Lite (Core + Site Policy + Default Excludes)
// @namespace    https://www.bianwenbo.com
// @version      0.7.1
// @description  极简字体美化:统一正文字体/等宽字体、可选平滑与字号缩放;全局与站点三态策略(启用/禁用/跟随);默认内置常见排除;带轻量设置面板;不改外链CSS以降低副作用
// @author       bianwenbo
// @match        *://*/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  /** -------------------------
   *  Storage & Defaults
   *  ------------------------- */
  const HOST = location.host;
  const KEY_GLOBAL = 'frLite:global';
  const KEY_SITE_PREFIX = 'frLite:site:';

  const DEFAULTS = {
    enabled: true, // 全局总开关
    fontFamily: `"Inter", "Segoe UI", system-ui, -apple-system, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft Yahei UI", "Microsoft YaHei", "WenQuanYi Micro Hei", Arial, Helvetica, sans-serif`,
    monoFamily: `ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
    smoothing: true, // 字体平滑
    scale: 1.0,      // 字号缩放
    site: {
      // 三态:true=仅本域启用, false=本域禁用, null=跟随全局
      enabled: null,
      fontFamily: null,
      monoFamily: null,
      smoothing: null,
      scale: null
    }
  };

  function deepClone(o){ return JSON.parse(JSON.stringify(o)); }
  function loadConfig() {
    const g = Object.assign(deepClone(DEFAULTS), GM_getValue(KEY_GLOBAL) || {});
    const s = Object.assign(deepClone(DEFAULTS.site), GM_getValue(KEY_SITE_PREFIX + HOST) || {});
    g.site = s;
    return g;
  }
  function saveGlobal(partial){
    GM_setValue(KEY_GLOBAL, Object.assign({}, GM_getValue(KEY_GLOBAL) || {}, partial));
  }
  function saveSite(partial){
    GM_setValue(KEY_SITE_PREFIX + HOST, Object.assign({}, GM_getValue(KEY_SITE_PREFIX + HOST) || {}, partial));
  }
  let CFG = loadConfig();

  /** -------------------------
   *  Effective Config
   *  ------------------------- */
  function effective(key){
    const v = CFG.site[key];
    return (v === null || v === undefined) ? CFG[key] : v;
  }
  function isEnabled(){
    const s = CFG.site.enabled;
    if (s === true)  return true;   // 本域强制启用
    if (s === false) return false;  // 本域强制禁用
    return !!CFG.enabled;           // 跟随全局
  }

  /** -------------------------
   *  CSS Injection
   *  ------------------------- */
  const STYLE_ID = 'fr-lite-style';
  const CLASS_ENABLED = 'fr-lite-enabled';
  const VAR_PREFIX = '--frl-';
  function cssString(v){ return v && typeof v === 'string' ? v : ''; }

  function buildStyle(){
    const font = effective('fontFamily');
    const mono = effective('monoFamily');
    const smoothing = !!effective('smoothing');
    const scale = Number(effective('scale')) || 1;

    return `
:root{
  ${VAR_PREFIX}font:${cssString(font)};
  ${VAR_PREFIX}mono:${cssString(mono)};
  ${VAR_PREFIX}scale:${scale};
  ${VAR_PREFIX}smooth:${smoothing?1:0};
}
html.${CLASS_ENABLED}{
  ${smoothing?'-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;'
             :'-webkit-font-smoothing:auto;text-rendering:auto;'}
}

/* 常见正文元素(排除常见“图标/公式/播放器/PDF水印”等) */
html.${CLASS_ENABLED} body :where(
  body, main, article, section, aside, nav,
  h1,h2,h3,h4,h5,h6, p, span, a, li, dt, dd, blockquote, q,
  div, label, input, textarea, button, select, summary, details,
  table, thead, tbody, tfoot, tr, th, td, caption,
  small, strong, b, i, u, s, em, sub, sup, mark, time, code, kbd, samp
):not(
  /* 图标/符号类库 */
  [class*="icon" i], [class^="icon" i],
  [class*="fa-" i], .fa, .fab, .fas, .far,
  .material-icons, .iconfont,
  [class*="glyph" i], [class*="symbols" i],

  /* 公式:MathJax / KaTeX */
  mjx-container *, .katex *,

  /* 视频/播放器 UI(video.js 等) */
  [class*="vjs-" i],

  /* PDF/水印层 */
  .textLayer *, [class*="watermark" i],

  /* 其它无需渲染的可视对象 */
  i[class], svg, [aria-hidden="true"]
){
  font-family:var(${VAR_PREFIX}font) !important;
  font-size:calc(1em * var(${VAR_PREFIX}scale));
}

/* 代码/等宽区域 */
html.${CLASS_ENABLED} pre,
html.${CLASS_ENABLED} code,
html.${CLASS_ENABLED} kbd,
html.${CLASS_ENABLED} samp{
  font-family:var(${VAR_PREFIX}mono) !important;
  font-size:calc(1em * var(${VAR_PREFIX}scale));
}

/* 输入控件也统一(不影响图标) */
html.${CLASS_ENABLED} input,
html.${CLASS_ENABLED} textarea,
html.${CLASS_ENABLED} select,
html.${CLASS_ENABLED} button{
  font-family:var(${VAR_PREFIX}font) !important;
  font-size:calc(1em * var(${VAR_PREFIX}scale));
}

/* 面板自身字体(即便 Shadow 下也尽量统一) */
#fr-lite-panel-root,
#fr-lite-panel-root *{
  font-family:var(${VAR_PREFIX}font), system-ui, sans-serif;
}
`.trim();
  }

  function applyStyle(){
    let node = document.getElementById(STYLE_ID);
    if (!node){
      node = document.createElement('style');
      node.id = STYLE_ID;
      document.documentElement.prepend(node);
    }
    node.textContent = buildStyle();
    document.documentElement.classList.toggle(CLASS_ENABLED, isEnabled());
  }

  /** -------------------------
   *  Minimal Panel (Shadow DOM)
   *  ------------------------- */
  const UI_ID = 'fr-lite-panel-root';
  let panelOpen = false;

  function openPanel(){
    if (panelOpen) return;
    panelOpen = true;

    const host = document.createElement('div');
    host.id = UI_ID;
    Object.assign(host.style, {
      position:'fixed', zIndex:2147483647, inset:'auto 16px 16px auto',
      width:'340px', borderRadius:'12px', overflow:'hidden',
      boxShadow:'0 8px 24px rgba(0,0,0,.18)'
    });

    const root = host.attachShadow({ mode:'open' });
    root.innerHTML = `
      <style>
        :host{ all:initial }
        .card{ background:#fff; color:#111; border:1px solid #e5e7eb }
        .hd{ padding:10px 12px; font-weight:600; background:#f8fafc; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; justify-content:space-between }
        .bd{ padding:12px; display:grid; gap:10px }
        label{ display:grid; gap:6px; font-size:12px }
        input[type="text"], input[type="number"], select{ padding:8px; border:1px solid #e5e7eb; border-radius:8px; outline:none; font-size:13px }
        .row{ display:flex; gap:8px; align-items:center; }
        .muted{ color:#666; font-size:12px }
        .btns{ display:flex; gap:8px; justify-content:flex-end; padding:10px 12px; border-top:1px solid #e5e7eb; background:#f8fafc }
        button{ padding:8px 12px; border-radius:8px; border:1px solid #e5e7eb; background:#fff; cursor:pointer }
        button.primary{ background:#111; color:#fff; border-color:#111 }
        .sep{ height:1px; background:#e5e7eb; margin:4px 0 }
      </style>
      <div class="card">
        <div class="hd">
          <div>Font Rendering Lite</div>
          <button id="closeBtn" title="关闭">✕</button>
        </div>
        <div class="bd">
          <label>
            本域策略
            <select id="sitePolicy">
              <option value="inherit">跟随全局</option>
              <option value="enable">仅本域启用</option>
              <option value="disable">本域禁用(排除此站点)</option>
            </select>
          </label>

          <div class="row" title="全局总开关(当本域策略=跟随全局时生效)">
            <input id="globalEnabled" type="checkbox">
            <span>全局启用</span>
          </div>

          <label>
            正文字体栈(逗号分隔,优先级从左到右)
            <input id="fontFamily" type="text" placeholder='e.g. "Inter", system-ui, "PingFang SC", ...'>
          </label>

          <label>
            等宽字体栈(代码区)
            <input id="monoFamily" type="text" placeholder='e.g. ui-monospace, Menlo, Consolas, ...'>
          </label>

          <div class="row" title="-webkit-font-smoothing + text-rendering">
            <input id="smoothing" type="checkbox">
            <span>启用字体平滑</span>
          </div>

          <label>
            字号缩放(0.8–1.5)
            <input id="scale" type="number" min="0.5" max="2" step="0.05">
          </label>

          <div class="sep"></div>
          <div class="muted">提示:站点字段留空则继承全局;“本域策略”可直接设置排除此站点。</div>
        </div>
        <div class="btns">
          <button id="resetSite">重置本域</button>
          <button id="resetGlobal">重置全局</button>
          <button id="saveBtn" class="primary">保存并应用</button>
        </div>
      </div>
    `;

    const $ = (id) => root.getElementById(id);
    const ui = {
      sitePolicy: $('sitePolicy'),
      globalEnabled: $('globalEnabled'),
      fontFamily: $('fontFamily'),
      monoFamily: $('monoFamily'),
      smoothing: $('smoothing'),
      scale: $('scale'),
      closeBtn: $('closeBtn'),
      resetSite: $('resetSite'),
      resetGlobal: $('resetGlobal'),
      saveBtn: $('saveBtn')
    };

    // 初始化
    ui.globalEnabled.checked = !!CFG.enabled;
    ui.sitePolicy.value = (CFG.site.enabled === true) ? 'enable' : (CFG.site.enabled === false ? 'disable' : 'inherit');
    ui.fontFamily.value  = (CFG.site.fontFamily ?? '') || '';
    ui.monoFamily.value  = (CFG.site.monoFamily ?? '') || '';
    ui.smoothing.checked = (CFG.site.smoothing ?? CFG.smoothing) === true;
    ui.scale.value       = (CFG.site.scale ?? CFG.scale).toString();

    // 事件
    ui.closeBtn.onclick = closePanel;

    ui.resetSite.onclick = () => {
      saveSite({ enabled:null, fontFamily:null, monoFamily:null, smoothing:null, scale:null });
      CFG = loadConfig(); applyStyle(); closePanel();
    };
    ui.resetGlobal.onclick = () => {
      saveGlobal(DEFAULTS);
      CFG = loadConfig(); applyStyle(); closePanel();
    };
    ui.saveBtn.onclick = () => {
      saveGlobal({ enabled: !!ui.globalEnabled.checked });

      const sitePolicy = ui.sitePolicy.value; // inherit / enable / disable
      const normalize = (v) => (typeof v === 'string' && v.trim() === '') ? null : v;

      saveSite({
        enabled: sitePolicy === 'enable' ? true : (sitePolicy === 'disable' ? false : null),
        fontFamily: normalize(ui.fontFamily.value),
        monoFamily: normalize(ui.monoFamily.value),
        smoothing: ui.smoothing.indeterminate ? null : !!ui.smoothing.checked,
        scale: (function(){ const n = Number(ui.scale.value); return isFinite(n) && n > 0 ? n : null; })()
      });

      CFG = loadConfig(); applyStyle(); closePanel();
    };

    document.body.appendChild(host);
    window.addEventListener('keydown', escClose, { once:true });
  }

  function escClose(e){ if (e.key === 'Escape') closePanel(); }
  function closePanel(){
    const host = document.getElementById(UI_ID);
    if (host) host.remove();
    panelOpen = false;
  }

  /** -------------------------
   *  Menu & Hotkeys
   *  ------------------------- */
  GM_registerMenuCommand('切换全局启用', () => {
    saveGlobal({ enabled: !CFG.enabled });
    CFG = loadConfig(); applyStyle();
    toast(`全局启用:${isEnabled() ? 'ON' : 'OFF'}`);
  });

  GM_registerMenuCommand('仅本域启用/跟随', () => {
    const cur = CFG.site.enabled;                 // true/false/null
    const next = (cur === true) ? null : true;    // true -> null -> true
    saveSite({ enabled: next });
    CFG = loadConfig(); applyStyle();
    toast(`本域策略:${labelSitePolicy(CFG.site.enabled)}`);
  });

  GM_registerMenuCommand('本域禁用/跟随(排除此站点)', () => {
    const cur = CFG.site.enabled;
    const next = (cur === false) ? null : false;  // false -> null -> false
    saveSite({ enabled: next });
    CFG = loadConfig(); applyStyle();
    toast(`本域策略:${labelSitePolicy(CFG.site.enabled)}`);
  });

  GM_registerMenuCommand('打开设置面板', () => {
    waitBody(openPanel);
  });

  // Alt/Option + X 打开/关闭面板
  window.addEventListener('keydown', (e) => {
    if (e.altKey && (e.key.toLowerCase() === 'x')) {
      e.preventDefault();
      if (panelOpen) closePanel(); else waitBody(openPanel);
    }
  });

  function labelSitePolicy(v){
    return (v === true) ? '仅本域启用' : (v === false ? '本域禁用' : '跟随全局');
  }

  /** -------------------------
   *  Utils
   *  ------------------------- */
  function waitBody(fn){
    if (document.body) return fn();
    const obs = new MutationObserver(() => {
      if (document.body){ obs.disconnect(); fn(); }
    });
    obs.observe(document.documentElement, { childList:true, subtree:true });
  }

  function toast(msg){
    const n = document.createElement('div');
    n.textContent = msg;
    Object.assign(n.style, {
      position:'fixed', right:'16px', bottom:'16px', zIndex:2147483647,
      background:'#111', color:'#fff', padding:'10px 12px', borderRadius:'10px',
      fontSize:'13px', boxShadow:'0 8px 24px rgba(0,0,0,.18)'
    });
    document.documentElement.appendChild(n);
    setTimeout(()=>n.remove(), 1500);
  }

  /** -------------------------
   *  Boot
   *  ------------------------- */
  applyStyle();

  // 动态页面:保持 html 上的启用 class
  const classGuard = new MutationObserver(()=>{
    document.documentElement.classList.toggle(CLASS_ENABLED, isEnabled());
  });
  classGuard.observe(document.documentElement, { attributes:true, attributeFilter:['class'] });
})();