前一篇《个人博客的代码块折叠/展开功能》讲了长代码折叠成 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