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