跳到正文

浏览器如何自定义默认字体(3):在原位置转换字体声明

浏览器如何自定义默认字体 · 1 · 2 · 第 3 篇(当前) · 4

全局换字体既要让普通正文采用个人偏好,也要保留网页独立声明的艺术字、代码与图标。逐元素写补丁会让新内容等待队列;在字体声明的来源处转换,后续节点便能继续由浏览器匹配与继承。

当前扩展先处理可读 CSSOM,再为符合条件的不透明公共外链准备镜像,最后对无法接管的声明使用 DOM 兜底。这里解释前两层怎样选择插入位置、保留层叠顺序和保存来源方案;元素兜底的更新与恢复见动态页面中的字体更新与验证。

CSS 的候选顺序、字重、斜体和缺字回退继续生效。插件先在可读 CSSOM 的原规则位置插入个人选择,再尝试为不透明外链建立可读镜像;仍无法处理的来源,才读取 getComputedStyle(element).fontFamily 写入局部补丁。字体资源仍由浏览器匹配,JS 不逐字绘制。

CSSOM 是浏览器暴露的样式表对象模型,可以读取规则、声明和分组关系。这里改的是当前文档内存中的属性,停用时撤销,不会修改网站服务器上的 CSS 文件。

Popup 展示正文和代码选择,后台保存偏好与一次字体撤销记录,同时负责受限的 CSS 获取与解析;内容脚本读取偏好并运行控制器。直接规则和远程镜像共用字体来源转换,DOM 保留最终兜底;各层分别记录原值与所有权。

先识别页面基础字体,再处理局部声明

只识别系统字体名称还不能覆盖所有阅读偏好。在 Grok 的一次实际检查中,英文界面文字使用 Universal Sans Text 400,并非微软雅黑。个人选择悠哉已经在候选列表里,但 universalSans、Inter 排在前面,英文继续由原站资源绘制。这符合原先保留独立字体的规则,却没有把整页基础文字统一起来。

继续给识别表添加字体名,只能处理眼前这个名称。更有用的判断依据是声明的作用范围:网站把某个字体用于 html、body 时,可以把它视为用户允许调整的基础字体设置,无需先判断这份字体在设计上是否属于艺术字。

Grok 把 .font-body 挂在 body 上,检查时页面中只有这一个元素使用该类。它通过 var(--font-universal-sans) 引用基础字体,变量展开后以 universalSans、Inter 开头。插件现在在这类基础声明最前插入个人字体,核心结果如下(省略 Emoji 回退):

.font-body {
  font-family: "Yozai", var(--font-universal-sans),
    ui-sans-serif, system-ui, sans-serif;
}

实现按选择器在当前文档中的实际匹配识别入口,不内置 .font-body 或 Grok 的变量名。只匹配根节点的规则在原位置前插;规则同时匹配局部元素时,保留共享声明,在根节点的计算值上兜底。后来有局部元素复用原本只供根节点使用的类,也会重新判断范围,撤销整条规则的首位前插,转为根节点兜底。

局部独立声明继续使用后面的保守规则。同一个字体家族可以同时用于正文与局部标题,出现在基础入口不意味着它的所有引用都要被前插。@font-face 资源描述符也保持原样。

生产扩展的受控页面:正文使用悠哉,局部艺术字保留楷体,字体图标保持 Font Awesome,代码使用独立偏好

原站若采用文字字体在前、图标字体在后的混合栈,插入正文用的字体仍保留原有图标回退;样本也验证了这个路径。安全边界同时依赖局部独立声明保留,以及具体字体的字形覆盖,不能把「插入」理解成任何字体组合都绝无影响。Grok 抽查的 Imagine 图标由 SVG 路径绘制,不依赖这条文字字体栈。

整条 font-family: var(--font) 还可能解析成 inherit 等全局值,不能直接拼成字体列表。此时保留原声明,在最多两个根节点上读取计算后的字体并前插,让后续文字正常继承。根补丁在来源处理前先还原,避免换字体时把旧偏好误记为作者原值;停止时再分别撤销各层仍持有的修改。

根节点之外,通用文字类也可能是基础入口

X 暴露了根节点判断的另一处边界:中文正文已经使用悠哉,导航里的英文仍保留原来的样式。实际检查时,html 和 body 的字体列表都以悠哉开头,但 .r-37j5jr 这条文字规则又声明了另一条栈:

font-family: TwitterChirp, Yozai, -apple-system, BlinkMacSystemFont,
  "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

这条规则当时匹配 402 个元素,分布在正文、导航和侧栏。中文样本实际绘制为 Yozai-Regular;英文导航样本仍使用网站下载的自定义字体,结合字体栈和注册的 Chirp 资源,可以确认它没有被悠哉接管。根节点的继承已经生效,子元素自己的字体声明却重新决定了候选顺序。

因此,基础入口也包含跨独立页面区域复用的通用文字规则。插件对 main、article、nav、aside、header 及对应 ARIA 角色读取有限文本样本:同一规则须覆盖一个内容区和一个互不包含的其他区域,并在两处都覆盖至少 3 个样本、样本数和文字量各占至少 60%。区域自身还须有至少 24 个字母、数字或汉字,字体列表中至少包含两个已知常规回退入口。图标的单字体声明、仅加一个 sans-serif 的声明和等宽栈不参与这项推断。

这些条件用来识别通用文字设置。大量同区段落、每处只有一个标题,或者文章内部的目录,都不足以满足条件。推断以原规则为单位:局部艺术字即使用了同一个字体家族,只要独立声明没有满足跨区域条件,仍保持原候选优先。匹配根节点又匹配局部元素的混合规则继续走根节点兜底,避免把这个例外变成整条规则的前插。

确认之后,仍在原 CSSOM 声明位置前插个人字体,完整保留 TwitterChirp 等原候选。新增元素直接匹配转换后的规则,不必逐个添加行内字体。类名、字体名和域名均不写入推断名单;后台镜像也使用相同逻辑。

生产 MV3 的受控页面:导航和正文共用一条字体规则,局部艺术字、字体图标及代码继续保留独立配置

动态页面还需要撤回过期判断。类名、角色、文字和 DOM 结构变化后会合并重判;区域消失时降级,异步出现时重新判断。验证中也发现,只移除导航而不新增文字时,原有文字候选队列可能没有任务,范围重判便不会执行。因此,来源范围的待办也参与调度和排空判断,即使没有新文字,也会检查一次;范围未变时不重扫样式来源,也不扫描未变化的整个父子树。

这仍是结构与字体栈共同提供的启发式信号。每次最多检查 24 个区域候选,保留 12 个有效区域,每处最多经过 384 个元素、读取 32 个文本样本;不读取布局尺寸,空闲时也不轮询。缺少语义区域、样本不足、只有单个回退入口或整条变量表达式的页面会保守保留原优先级。图标字体若也使用完整文字回退并大范围承载普通字符,仍可能误判,不能由这次 X 的案例推导出所有页面都能自动分类。

局部声明识别默认入口,再补上末尾回退

局部声明中在已知入口前插入偏好,未知列表末尾追加,原字体与图标资源继续参与回退

例如,局部声明的原列表为 "Punctuation SC", "Inter", system-ui, sans-serif,选中的正文家族是 "LXGW Neo XiHei Plus"。转换后变成:

font-family:
  "Punctuation SC", "Inter", "LXGW Neo XiHei Plus",
  system-ui, sans-serif;

前两个字体的覆盖仍优先;个人字体在系统入口前补充中文。原来的后备也都还在,所选字体缺字时继续回退。

实现用一份常规入口集合识别微软雅黑、苹方、Segoe UI、Arial 等具体名称,以及未加引号的 system-ui、-apple-system、BlinkMacSystemFont、ui-sans-serif、sans-serif 和 serif。遇到第一个目标,就在它前面插入。这样才能替换本机已经安装的默认字体。

没有已知入口的完整字体列表,则保留全部原候选,在末尾追加正文偏好。例如只写了 "Unlisted Font",配置悠哉后变成:

font-family: "Unlisted Font", "Yozai";

原字体存在且能提供当前字形时,继续使用它;原候选都无法提供字形时,再尝试悠哉。艺术字体和图标字体也按这个顺序处理,JS 不需要先猜它们的用途。如果原列表中已有通用字体并提供了字形,末尾候选也不会越过它;个人字体仍不可用或缺字时,浏览器继续最终回退。CSS Fonts 的字体匹配规则定义了这种逐字形选择。

具体名称还要区分家族名和字面名。虎扑论坛曾在右侧列表单独写 font-family: PingFangSC-Regular,表头则写 PingFangSC-Medium,都没有通用兜底。Windows 没有匹配到这份苹方时,回到了浏览器设置中的汉仪旗黑;正文偏好虽然选了悠哉,旧识别表却只包含 PingFang SC,来源层和 DOM 层都漏掉了同一个名称。

因此,常规入口也包含苹方 SC、TC、HK 的 Regular、Medium、Semibold、Light、Thin、Ultralight 字面名,识别 PingFangSC-Regular 以及带空格的完整写法。苹果字体清单列出了这些地区版本与字重。它们共用候选转换,来源处改写后,右侧子节点正常继承;不需要为虎扑增加选择器。末尾兜底也能接住 Windows 上缺失的未知字面名,但在装有苹方的 macOS 上,要主动采用个人选择,仍需把它识别为默认入口。名称匹配保持完整相等,Brand PingFangSC-Regular、PingFangSC-Regular Icons 等独立名称只在末尾增加后备。仅检查「没有报错」不够,回归样本还要核对实际绘制资源。

列表不能直接用 split(',') 拆分,因为家族名称本身可以带逗号或转义。解析器保留引号和原始片段,比较名称时再解码 CSS 转义;"system-ui" 也会与关键字 system-ui 区分。插入位置之前已有个人选择时不重复添加,重复计算先还原旧补丁,不会无限叠加所选家族。

inherit、initial、unset、revert、revert-layer 是整条属性的全局值,不能拼成 inherit, "Yozai"。没有已知入口、且仍含 var() 等未解析表达式时,也先保留表达式,沿字体变量来源处理,或交给 DOM 层读取计算后的列表;写回 CSSOM 前再验证新值语法。这样保留原来的继承和层叠语义。CSS 变量的计算值规则也说明了为什么不能只看变量声明表面上是否有效。

这是一套保守的候选转换规则,无法自动识别字体的设计意图。如果网站把 Arial 用作标志,或把艺术字体放在目标入口之后,它仍可能被影响;需要恢复原站时,可以停用扩展并刷新网页。图标应有独立的字体声明,私用区字符也会被保守跳过,不能把类名识别当成可靠的字体分类。

三层来源处理与候选顺序是两件事。前者决定从哪里读取和改写,后者决定个人选择插在哪里:基础入口首位前插,局部声明按已知默认入口插入,未知局部列表在末尾兜底。局部已经可用的独立字体仍优先,不需要给每个新名字判定设计用途。来源层、后台镜像和 DOM 兜底共用这些转换规则。

优先改写声明来源,让新增节点自动匹配

可读的样式表可以直接遍历 cssRules。普通样式规则里读取 font-family,转换候选后仍写回同一个声明;省略所有权记录后的核心操作是:

const previous = style.getPropertyValue('font-family');
const priority = style.getPropertyPriority('font-family');
const next = baseEntry
  ? prependTextFamily(previous, preferences)
  : transformFamily(previous, preferences, false);
if (next) style.setProperty('font-family', next, priority);

规则留在原位置,选择器、@media、@supports、@layer 和原来的 !important 继续由浏览器处理。没有把所有改写都提高成重要声明,也没有把它们复制到样式表末尾;后者可能改变原本的艺术字体与主题规则之间的顺序。CSS 的层叠规则决定了这些区别。

正文使用变量时,插件从字体声明和 font 简写中收集 var() 引用,找到自定义属性的定义,再沿依赖继续查找。例如 font-family: var(--copy)、--copy: var(--base)、--base: Inter, system-ui,只需转换最后那份候选。变量名由规则发现,不需要内置知乎或 VitePress 的名称。

可读 CSSOM 优先,不透明外链按需取源,无法安全接管的声明由 DOM 兜底

只转换能按字体列表解释的值。局部独立字体的候选优先级继续保留,@font-face 的资源描述符不会被当作元素声明修改。变量如果也用于 content 等其他属性,或者有已知的 @property 类型限制,就保留变量定义;基础入口可在字体声明本身前插,整条表达式或其他特殊声明则读取计算值兜底,避免把页面文字或其他类型的值一起改掉。程序化注册、特殊语法和不可见来源仍有识别边界。

没有显式字体声明的文档,用放在作者样式之前的低权重根字体规则补充默认选择。作者给独立标题等元素明确指定的字体,仍按正常层叠生效。

样式来源也有权限边界。部分跨域样式表的 cssRules 会抛出 SecurityError,不能因为浏览器已经显示这份 CSS,就认为内容脚本一定能读取它。CSSOM 的访问约束要求检查来源的可读状态。插件隔离受限来源,继续其他规则;符合条件的外链进入后台取源层。

来源处处理成功后,新增的正文段落在插入瞬间就能继承偏好字体,不需要等待扫描器给它逐一添加 style。DOM 兜底仍会检查特殊声明和代码语义,不能把「没有行内样式」当成字体失效的判断依据。

后台能取回 CSS,接管时仍要保留原位置

内容脚本受页面来源的网络约束,扩展后台则能在具备对应主机访问能力时获取跨域资源。Chrome 的跨域请求说明给出了这条路径。当前 manifest 显式声明 HTTP/HTTPS 主机权限,与网页处理范围一致;接口只接受自身活动 HTTP/HTTPS 文档的样式任务,没有向网页开放通用 URL 获取入口。顶层与子框架的任务、取消令牌和 CSS 响应缓存按标签页、框架及文档身份隔离。

后台取回的是另一份资源文本。页面原来的 CSSStyleSheet 仍然不可读,额外请求不会改变它的来源状态。新文本也不包含网站随后只在内存里做过的 CSSOM 修改,因此不能把「取得响应」等同于「完整复制当前运行时样式表」。

如果只提取字体规则并追加到最后,原来排在外链之后的艺术字体可能被反过来覆盖;重要声明、条件和层也可能发生变化。因此这里为普通外链准备完整镜像,放在原 link 紧后面,保留非字体声明的相对顺序。镜像先设为 media="not all",解析和字体转换完成、且确有转换需求后,才切换来源。

镜像紧跟原 link,准备完成后切换媒体门控,后面的作者样式仍然在后面

切换时保存原表的媒体条件,临时将原表媒体设为 not all,让镜像使用保存的条件。这个同步阶段不修改 link.disabled;网页作者的禁用、恢复和媒体变化,在后续相关重算中继续跟随。换链接、移除节点,或两个字体均跟随网页时,撤销镜像,并恢复仍属于扩展的门控。单纯 CSSOM 标志变化没有可观察事件时,仍可能需要手动重新应用。

外链里还有导入和相对资源地址。后台用 PostCSS 与值解析器按原顺序展开 @import,保留命名层、匿名层、supports() 和媒体条件;图片与字体 URL 以各自来源为基址改成等价绝对地址。字体资源的家族映射保持原样。例如:

@import "./base.css" layer(base) supports(display: grid)
  screen and (min-width: 600px);

导入内容继续位于 base 层、支持条件与媒体条件之内,不会变成一组无条件的末尾重要声明。匿名层分别建立,不能因为都没有名称就合并;重要声明的层优先级与普通声明不同,必须一起验证。导入与层叠规范定义了这些关系。

生产 MV3 在跨域构造页中的实际结果:正文选择汉仪旗黑,艺术字体与后置规则仍保留

镜像也会新增一个 style 节点。因此当前只处理 head 内的普通外链;body 内资源继续使用 DOM 兜底,避免改变正文的 :nth-child 等结构语义。已有镜像的 link 被移入正文时,也会撤销镜像。

后台请求不附带 Cookie,响应须为 CSS;私有缓存策略、依赖失败、重定向、命名空间、alternate/title 样式及超出预算的来源,不建立镜像,继续保留原表和 DOM 兜底。可读表内部没有独立 DOM owner 的不透明导入,当前也保留兜底,不擅自移到整表末尾。适用对象是可重新获取的公共样式,不承诺与所有依赖身份或运行态的响应完全相同。

网络与解析也有预算:单文件最多 1 MiB,展开后最多 2 MiB,每任务 8 秒、最多 6 层和 32 次导入展开。同文档的相同请求去重;公共响应按 max-age/Age 缓存最多 2 分钟,不跨文档复用,内存总量上限 8 MiB。没有新鲜度的响应继续交给浏览器请求机制。失败不会随每次 DOM 更新反复请求,用户可通过「重新应用」重试。

取源、消息与解析都需要时间,不能代替提前注入,也不保证首屏没有切换。在镜像准备期间,现有 DOM 兜底继续工作;接管后重算,会撤销已不需要的逐元素补丁。取源层不会上传本机字体目录或偏好。

保存来源方案,下次校验后复用

同一份 CSS 每次访问都重新寻找字体声明、追踪变量用途,会重复做相同工作。可以把这部分分析保存下来,下次确认来源仍相同时,直接定位当前声明。缓存保存字体声明的位置、安全变量的定义位置和结构依赖范围;字体资源、转换后的 CSS 和逐节点样式都不写进方案。

一次复用按以下顺序完成:

  1. 读取当前有序 CSSOM,用来源身份、原始声明、分组条件和规则顺序生成摘要。扩展已经改过的值,在离线声明副本中还原成作者基线,避免摘要被自己的字体补丁影响。
  2. 按当前 origin(协议、主机名与端口)、摘要和方案版本查找缓存,再校验声明位置与属性。命中后复用来源分析结果,继续用当前 DOM 判断根入口和跨区域文字范围。
  3. 在当前声明的原位置应用本次字体选择。原有选择器、层、媒体条件和 important 优先级继续决定层叠;新增节点仍由 CSS 自动匹配。

同一套 CSS 可能同时服务有导航的列表页与单栏详情页,旧页面的跨区域判断不能直接搬过来。因此缓存命中也要重判当前作用范围,类名、角色、文字和结构变化后继续按需检查。它减少完整发现和变量分析,不等于停用 JS 或给整个域名永久贴上「纯 CSS」标签。

变量可以跨样式表引用,方案以当前来源图为单位。来源内容、顺序或条件变化后,该图重新分析;即使规则数量没变,新增的非字体用途也会使摘要改变,重新检查变量是否安全。这里只在必要的来源刷新中核对当前内容,没有空闲轮询;未产生可观察事件的 CSSOM 属性改写,仍可能需要「重新应用当前页」。这个按钮也会主动跳过缓存重新学习一次。

本机最多保留 48 项、条目 JSON 的 UTF-8 体积不超过 1 MiB。还有不透明来源、超过 10000 条规则,或摘要输入超过 524288 个 JS 字符时,继续正常发现和兜底,不生成持久化方案。方案稳定后延迟保存,字体应用不会等待落盘。摘要用于内容变化检测,不承担安全认证;方案不包含整份 CSS、页面文字、路径、查询参数或个人字体选择。

代码通过语义入口独立配置

普通代码取 pre、code、kbd、samp、tt。先按外层到内层处理代码容器,内部 token 再读取真实继承值;已经继承所选代码字体时不重复添加补丁,只有独立声明需要额外处理。代码选择放在最前面,正文选择负责补字,最后保留原站候选:

font-family: "Ligconsolata", "LXGW Neo XiHei Plus", Consolas, monospace;

这个例子保留了原来的 Consolas, monospace。按钮和明确的图标语义不按代码强制覆盖,正文规则也无需另一长串 :not(...) 与代码规则竞争。只启用代码偏好时仍保留原站正文;未选择代码字体时,也不会由代码分支强制改写。

DOM 兜底跳过 .monaco-editor、.ace_editor、.CodeMirror 这些已知编辑器容器,包括内部的 pre,样式表层也跳过明确指向这些容器的规则。祖先声明带来的正常继承仍可能影响内部未独立指定字体的文字。它们可能自行测量字宽、控制光标和工具栏,字体偏好需要按编辑器接口另行适配;其他自管度量的编辑器也应分别检查。

总结

来源转换需要确定个人字体的插入位置,并保持原站的选择器、条件和层叠。基础入口前插个人偏好,局部声明在已知默认入口前插入,未知局部列表则追加兜底;这套判断由来源层、镜像层和 DOM 层共用。

可读 CSSOM 在原位置写回,公共外链镜像保留导入、资源基址和顺序,方案缓存只复用经过校验的来源分析。不能安全接管的声明继续交给 DOM 层,字体名判断、网络预算、变量用途和编辑器度量仍各有边界。

上一篇:安装字体偏好扩展与管理设置。下一篇:动态页面中的字体更新与验证。