正文标题自动编号,与目录编号共用同一份计算 #30

需求:目录里有编号、正文标题没有,读长文时要回头去目录找对应关系。
本版给 h1/h2/h3 加编号,样式为灰色小号(标题字号的 0.62 倍),
不抢标题视觉重心,与目录 .yang-toc-num 同风格。

编号规则(编号范围跟着后台 toc_levels 设置走,默认 1/2/3):
- 按「实际出现的最外层」归一,没有 h1 时 h2 直接是一级(1/2/3),
  不会编成 2/2.1。天然满足用户"h1 没有就是 h2 一级"的要求。
- 作者已手写编号的标题跳过并占位(1./1、/1)/1.2/一、/(一)/第一章
  等写法),不叠加成"3.1 1. xxx",但编号序列不错位。

实现要点:
- 抽出 yang_toc_pick / yang_toc_depths / yang_toc_number_map 三个纯函数,
  目录与正文共用同一份编号计算,杜绝两边漂移(用户痛点正是"两边不一致")。
- 插入按 id 回填:条目清单只来自 scan() 那一份,id 是唯一键。
- 用 preg_match_all + PREG_OFFSET_CAPTURE 拿字节偏移、从后往前拼接插入。
  早期写成 preg_replace_callback('#<h([1-6])([^>]*)>(.{0,120})#is'),
  那个 .{0,120} 贪婪跨过块边界,导致 5 个标题只有第 1 个拿到编号且不报错。
- 幂等用 strncmp(substr($html,$end), $needle, strlen($needle)),不手数字节
  (写成固定 26 而实际 25 会让幂等静默失效、编号插两层)。

CSS 修一个自己造的问题:编号把标题撑高 3px。
line-height:inherit 无效——同行高下小字号半行距更大,盒底反而更往下探
(h2 46→49)。改为 display:inline-block + line-height:1 成为原子行内盒,
实测 46→46、37→37。八组隔离试验数据见 _tooling/_diag362b.js。

验证(全部本地/线上实跑,非推断):
- 正向判据 43/0;反证 22/0(10 组注入均红在预期条目)
- 线上 4 篇真实文章 20/0(含"只有 h3"、"h1+h2"、"h2+h3"三种形态)
- 新旧两版目录 HTML 逐字节比对 7/0(4/4 篇相同,证明重构没改坏老行为)
- 几何判据 68/0,含两条反证:回到 inline+inherit ⇒ 高度差 3.00px;
  编号 display:block 掉行 ⇒ G3 差 14.88px
- 打包自检 59/0 + 4 组注入反证全红
This commit is contained in:
2026-10-28 08:27:53 +08:00
parent 264a6ec06a
commit 76c69b31e8
3 changed files with 398 additions and 59 deletions
+82
View File
@@ -221,6 +221,88 @@
margin: 0;
}
/* ── 正文标题的自动编号(3.2.62 / 工单 #30)
*
* 结构由 PHP 插入:<h2 id="x"><span class="yang-h-num">1.1</span>标题</h2>
* 编号是**独立 span**,不是改标题文本 —— 所以不进编辑器、不进 RSS、
* 不进搜索结果、不进摘要。作者手写编号时压根不插这个 span。
*
* ── 为什么用灰色小号(用户选定的方案)──────────────────────────
* 标题的核心是**文字本身**,编号只是索引辅助。若与标题同色同粗,
* 视觉上"1.1"会和标题正文抢注意力,长文章里满屏都是粗体数字。
* 灰色 + 小一号让编号退到背景,眼睛扫标题时能忽略它、需要定位时
* 又能看见 —— 这也与目录里 `.yang-toc-num` 的处理一致(同为灰色小字),
* 正文和目录的编号看起来是同一套东西。
*
* ── 字号为什么要跟标题走而不是写死 ─────────────────────────────
* 编号要跟着标题一起缩放才有"附属于标题"的感觉。这里用 em:
* 0.62em 在 h2(24px)上是 14.9px、h3(20px)上 12.4px,
* 正好落在 12~15px 这个"能看清但不抢眼"的区间。
* ⚠️ 若写成固定 px,h2 的编号会显得太小、h3 的又偏大。
*
* ── font-weight / font-style 必须显式重置 ────────────────────
* span 会继承标题的 font-weight(700/600)和可能的斜体。
* 不重置的话"灰色但加粗"仍然很重,弱化就白做了。
*
* ── margin-right 用 0.35em 而不是固定 px ────────────────────
* 同样是为了跟标题字号联动;h2 的 24px ⇒ 8.4px,h3 的 20px ⇒ 7px,
* 在两级上都刚好是"一个空格多一点"的呼吸感。
*
* ── 🔴 为什么必须 `display: inline-block`(这是本规则最不直观的一处)──
* 小字号的行内元素会把行盒**撑高**,实测 h2 46 → **49px**、h3 37 → **40px**,
* 每级都多 3px。3px 不起眼,但它出现在**每一个**标题上:长文 10 个二级标题
* 就多出 30px,h2 的灰底跟着变高,整篇标题的节奏全变;更麻烦的是它**只在
* 该标题有编号时出现**,于是「有编号的文章」和「没编号的文章」行距不一致,
* 极难归因。
*
* ⚠️ 我先试过 `line-height: inherit`(以及写死 46px)—— **完全无效**,
* 仍 49px。原因值得记下来,因为直觉是反的:
*
* 同行高下,行内盒的上下边界由「半行距」决定,而半行距
* 把 content-area **居中**塞进 line-height:
*
* 盒底 = baseline + desc×fs + (L − fs) / 2
* h2 (fs=24, L=46): 0.25×24 + (46−24)/2 = 17.00
* span(fs=14.88,L=46): 0.25×14.88+ (46−14.88)/2 = 19.28 ← 多探 2.28
*
* **字号越小,半行距越大**,盒底反而越往下走。所以「让两者共用行高」
* 不是解药,恰恰就是病根 —— line-height 怎么调都救不回来。
*
* ⚠️ `vertical-align` 也试过:`baseline` 仍 49px;`middle` 48.52px。
*
* ✅ 真解:把编号变成**原子行内盒**(inline-block),它就只按自身内容
* 高度参与行盒,不再受半行距拉扯。配 `line-height: 1` 让盒高正好
* 等于字号(14.88px),基线落在 asc 处 —— 上下都远在 strut 之内,
* 实测 h2 46→**46**、h3 37→**37**,一分不多。
* (八组隔离试验的完整数据见 `_tooling/yang_theme_test/_diag362b.js`。)
*
* 副作用检查:inline-block 不会跨行断开 ⇒ 编号永远留在标题第一行开头,
* 正是想要的;`white-space: nowrap` 随之只作用于这个原子盒,不会影响标题文字。
*
* ⚠️ 归档页 `h3.ar-year` 同样不会带编号 —— 编号是在 the_content
* 过滤器里按「目录条目」插的,而归档短代码的年份标题不在
* yang_toc_scan() 的结果里(它们是 [Archives] 渲染时才产生的),
* 所以天然不受影响。评论区 h3 同理(不在 .content 内)。 */
.k-main .details .article .content .yang-h-num {
/* 🔴 原子行内盒:不参与行盒的半行距计算,标题高度才不会被撑高(见上) */
display: inline-block;
line-height: 1;
margin-right: 0.35em;
color: #a8abb2;
font-size: 0.62em;
font-weight: 400;
font-style: normal;
/* 数字等宽,1.1 和 3.10 宽度一致,多行标题才不会看着参差 */
font-variant-numeric: tabular-nums;
/* 编号自身不换行(inline-block 本来也不拆,写上是为了并列时也不拆) */
white-space: nowrap;
}
/* 暗色站点:跟目录序号同一个灰,否则 #a8abb2 在深色底上过亮 */
html.yang-dark .k-main .details .article .content .yang-h-num {
color: #767d88;
}
/* 列表缩进与行距 */
ul {
margin-left: 2em;