🤖
AI审核中

代码块里多出来的那截颜色:行内底色、color-mix 兜底与逐像素回归

换到流萤主题后,代码块里多出两种颜色:每行有字的地方垫着一块底色,折叠时底部渐隐和「展开全部」条偏蓝。原因都是组件底座写死了藏青色,而主题的代码底色换了。本文记录排查和修法:行内元素的背景为什么一行一画;想保留渐隐曲线只换颜色时用 color-mix,以及带 var() 的 color-mix 在旧浏览器里会整条失效、兜底必须写在后面的 @supports 里;用自写 PNG 解码逐像素比较,证明其它主题一个像素都没变;Playwright 定位器重新查询造成的假「点了没反应」;AI 助手黑夜里的同类问题。

文章摘要

换到流萤主题后,代码块里多出两种颜色:每行有字的地方垫着一块底色,折叠时底部渐隐和「展开全部」条偏蓝。原因都是组件底座写死了藏青色,而主题的代码底色换了。本文记录排查和修法:行内元素的背景为什么一行一画;想保留渐隐曲线只换颜色时用 color-mix,以及带 var() 的 color-mix 在旧浏览器里会整条失效、兜底必须写在后面的 @supports 里;用自写 PNG 解码逐像素比较,证明其它主题一个像素都没变;Playwright 定位器重新查询造成的假「点了没反应」;AI 助手黑夜里的同类问题。

前一篇《个人博客的代码块折叠/展开功能》讲了长代码折叠成 10 行、底部加渐隐和「展开全部」条。最近换到流萤主题后,在黑夜模式下截图一看,代码块里多出了两种颜色:

  • 每一行有字的地方都垫着一块颜色略不同的底,空白处没有;
  • 折叠时,底部渐隐的末端和「展开全部」那一条是偏蓝的藏青,代码区却是近乎黑的灰。

白天也有,只是对比小、不明显。两处是同一个原因:组件底座里写死了藏青色,而主题的代码块底色换了。

一、每行一块底色:行内元素的背景

站点的代码块结构是 .code-wrapper > .code-body > pre > code。底座样式里有这样一条:

.code-body, .code-body pre, .code-body pre code, .hljs {
    background: #0f1828 !important;
}

后面另一组规则把 .code-body 改成了主题变量 var(--code-bg)、把 pre 改成透明,唯独漏了 code。code 是行内元素,行内元素的背景按「行盒」画:每一行只画到这一行最后一个字符,行首的缩进也算在内。于是代码区的底色是主题的,每行文字下面却垫着一条 #0f1828。

主题 代码块底色 写死的底色
流萤白天 #14161f #0f1828
流萤黑夜 #0d0f16 #0f1828
纸墨黑夜 #141210 #0f1828
星河、格致、画报 #0f1828 #0f1828

星河、格致、画报的代码底色本来就是 #0f1828,所以一直看不出来;纸墨黑夜以前单独补过一条透明规则;流萤是新主题,没人补。

修法是在共享样式里把 pre 里的 code 设成透明。两条都带 !important、选择器优先级也一样时,后写的赢,所以这条要放在写死的那条之后:

.code-body pre code, .code-body pre code.hljs {
    background: transparent !important;
}

纸墨自己补的那条随之删掉。验证时不只看 code,而是把 pre 里每个元素的计算背景都取出来,凡是不透明、又和代码块底色不同的都算失败。六套主题深浅色共 12 种组合,改完都只剩代码块本身的底色。

二、折叠渐隐和展开条:一个颜色,三种写法

折叠后的渐隐是 .code-body::after 上的一层渐变,从透明过渡到代码底色,盖住第 10 行往下的字;下面接「展开全部」条。查层叠时发现,这两样各有三条规则在抢:

  • 一条用主题变量,但没加 !important;
  • 一条写死 #101828,带 !important;
  • 一条写死 #0f1828,也带 !important,写在更后面。

生效的是最后这条。它的渐隐是这样写的:

background: linear-gradient(180deg, rgba(15, 24, 40, 0), rgba(15, 24, 40, .72) 58%, #0f1828 100%) !important;

在 58% 的位置就已经 72% 不透明,比一条直线的渐变更快遮住字。我想只换颜色、保留这条曲线,于是需要「主题色加 72% 不透明度」。主题色是变量,里面是十六进制,拆不出三个通道,rgba(var(--code-bg), .72) 不成立。能用的是 color-mix():

.code-wrapper.collapsed .code-body::after {
    background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--code-bg) 72%, transparent) 58%, var(--code-bg) 100%) !important;
}

和透明色按 72% 混合,结果就是原来的颜色、不透明度 0.72。Chrome 算出来的值是 color(srgb 0.0784 0.0863 0.1216 / 0.72),三个通道正是流萤白天的 #14161f。

兜底不能写在前面

color-mix() 要 Chrome 111、Safari 16.2 以后才支持。常见的兜底写法是旧值写在前、新值写在后,不认新语法的浏览器丢掉后一条,前一条照样生效。这里不行:

一条声明里只要有 var(),浏览器在解析时就不检查它的值,一律当作有效收下;等到计算样式、把变量代进去时,才发现 color-mix 不认识。这时这个属性按「计算时无效」处理,退回初始值(背景透明),不会回头去用前面那条声明。结果是旧浏览器上渐隐整个消失,字被直接截断。

所以兜底要用 @supports 单独写,并且放在后面:

@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .code-wrapper.collapsed .code-body::after {
        background: linear-gradient(180deg, transparent, var(--code-bg) 100%) !important;
    }
}

旧浏览器上曲线变成直线,但颜色是对的。这条规则在新浏览器里永远不生效,平时测不到。验证办法是在测试里把样式表中的 @supports not 改成 @supports,让它在 Chrome 里生效,再量一遍:渐隐末端和代码底色一致,计算值里也没有 0.72,说明兜底规则确实接得上。

悬停色用一个带默认值的变量

展开条直接取 var(--code-bg)。鼠标悬停时原来是 #121e31,比 #0f1828 亮一点、也更蓝一点。如果用 color-mix 往代码底色里混一点白,流萤是对了,可星河、格致、画报的悬停色也会跟着变,而这三套本来没问题。最后改成一个带默认值的变量:

background: var(--code-expand-hover, #121e31) !important;

只有流萤给了值(白天 #191b25,黑夜 #13151e,按和代码底色的亮度差与原来那一档相近来挑);其它主题没定义这个变量,仍然用默认的 #121e31。

三、用像素证明「别的主题没变」

改的是六套主题共用的样式,最怕修好了流萤、碰坏了别人。计算样式只能说明某个属性的值,看不出画出来的结果,所以这次直接比截图的像素。

Playwright 的截图是 PNG,Node 自带 zlib,自己写一个最小的解码器就够用:读出 IHDR 里的宽高,把所有 IDAT 拼起来解压,再按每行开头的过滤类型(无、Sub、Up、Average、Paeth 五种)还原像素,四十行左右。取色时不取单个点,而是在一整行上每隔 5 像素取一次、取出现次数最多的颜色,避开文字的抗锯齿:

  • 代码底色:pre 上内边距里的一行,那里没有字;
  • 渐隐末端:代码区最底下一行;
  • 展开条:条的左边三成,字在中间。

改之前先在线上把 12 种组合各截一张(静止和悬停两种状态);再把线上带哈希的样式表拦下来,换成本地改过的文件,同样截一遍;然后逐像素比较,任一通道差超过 1 就算不同:

截图 不同的像素 所在行
星河、格致、画报 0 —
纸墨白天、云笺 0 —
流萤白天 99936 256~366
流萤黑夜 104356 252~366
纸墨黑夜 37607 252~320

前两行每套都包括深浅色、静止和悬停,全部一个像素不差。流萤的截图高 368,变化只落在最下面 110 来行:渐隐的 72 像素加上 46 像素高的条。纸墨黑夜的条原来就对,只有渐隐变了,所以只到 320 行。

四、一个假的「点了没反应」

测展开以后的「收起代码」条时,脚本报告:点了「展开全部」,代码块还是折叠的,按钮上的字也没变。不拦截样式表、直接在线上跑,结果一样,看上去像是站点本身的 bug。

在页面里给 document 挂上捕获阶段的监听,把 pointerdown、mousedown、click 和 class 的变化按时间记下来:

  • 41 ms:click,目标是条里的 span;
  • 42 ms:代码块的 class 变成 code-wrapper,collapsed 已经去掉。

点击是生效的。问题出在测试脚本的定位器上:

const wrap = page.locator('.code-wrapper.collapsed').first();
await wrap.locator('.code-expand-btn').click();
// 之后再用 wrap 读状态

Playwright 的定位器不是元素引用,每次使用都按选择器重新查一遍。第一块展开后不再带 collapsed,「带 collapsed 的第一块」就成了文章里的第二块,量到的是另一块代码。改成先给要测的那块打一个 data-* 记号、再按记号定位,展开前后就是同一个元素了。

五、AI 助手里也有一份

回归时顺带看了文章页的 AI 助手。它的回答里如果有长代码,也会被同一个脚本折叠,但用的是另一套颜色(One Dark 的 #282c34)。往消息列表里塞一条结构和真实回答一样的消息,调用站点自己的折叠函数,再量一遍:

  • 白天:代码区、渐隐、展开条都是 #282c34,一致;
  • 黑夜:代码区被另一条黑夜规则改成了 #171d26,渐隐和展开条还是 #282c34,底部多出一截亮的。

问题一样,方向反过来:这次是渐隐比代码区亮。在那条黑夜规则旁边补上渐隐和展开条的颜色(#171d26,悬停 #222936),五套新主题的黑夜都量过,一致。

小结

  • 行内元素的背景一行一画,只盖到有字的地方。pre 里的 code 带了底色,看到的就是一条条色带。
  • 主题色一旦是变量,所有用到它的地方都要跟着变量走:底色、渐隐、按钮条、悬停色。漏掉一处,就会在底色不同的主题里露出来。
  • 带 var() 的 color-mix() 在旧浏览器里不会退回前一条声明,而是整条失效。兜底要写在后面的 @supports not 里,并且要想办法让它在测试里真正跑一次。
  • 改共享样式时,拿改前改后的截图逐像素比较,可以直接证明没动到的主题一个像素都没变。
  • Playwright 的定位器每次都会重新查。选择器里带着会被操作改掉的状态类名时,先打记号再定位。
0 条评论

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧

评论区 0

头像
 0 条评论