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

极简字体美化:统一正文字体/等宽字体、可选平滑;根字号可选缩放(不改站点局部字号);三态站点策略;默认内置常见排除;轻量面板;不改外链CSS。

Stan na 10-09-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Font Rendering Lite (Core + Site Policy + Default Excludes)
// @namespace    https://www.bianwenbo.com
// @version      0.7.2
// @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: `"Microsoft YaHei", "Microsoft Yahei UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", system-ui, -apple-system, Arial, Helvetica, sans-serif`,
    monoFamily: `ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
    smoothing: true, // 字体平滑
    scale: 1.0,      // 根字号缩放(仅影响 rem;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;'}
  /* 根字号缩放:仅影响 rem,不主动覆盖站点的 px / 局部 em 设计 */
  font-size: calc(100% * var(${VAR_PREFIX}scale));
  /* 如需进一步统一视觉,可尝试启用(默认关闭以保守): */
  /* font-size-adjust: from-font; */
}

/* 常见正文元素(排除常见“图标/公式/播放器/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;
}

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

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

/* 面板自身字体(即便 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. "Microsoft YaHei", "PingFang SC", system-ui, ...'>
          </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,仅影响 rem)
            <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'] });
})();