一、三级大纲贴进去,编号从第一个数一路续下去
上周整理一份 demo-需求清单,我把带三级编号的大纲整段贴过去,本指望能还原「3. 起步、4. 第二步」那种观感。结果第一行是 3,往后全被续成 4、5、6;更蹩脚的是有几行直接变成 2、3、4,我写的 9 和 30 像没存在过。回去把这页自研解析器(markdown-to-html.html,1135 行、41623 字节,只有 1 个 script、18 个元素 id,外部依赖没引)读了一遍,才把「它到底怎么数编号」捋清楚——它管的是两件我从来没留意过的保真问题:有序列表的起始编号,和标题锚点的 id。
这页把「有序列表起始序号」写成一条很干脆的判据:只有当列表第一项的编号不等于 1 时,才给 <ol> 补一个 start 属性;第一项是 1,就什么都不写,交给浏览器从 1 续。我把 Markdown 转 HTML 这页的这条规则拆开跑了几组,输出对照如下。
| 输入的有序列表 | 这页实际生成的标签 | 浏览器最后渲染成 |
|---|---|---|
3. 起步 / 4. 第二步 | <ol start="3"> | 3、4(第二项的 4 不写出,靠续) |
1. 甲 / 1. 乙 | <ol> | 1、2(第一项是 1,不留 start) |
01. 甲 / 02. 乙 | <ol> | 1、2(前导零被 parseInt 吃掉) |
2. 甲 / 9. 乙 / 30. 丙 | <ol start="2"> | 2、3、4(9 与 30 整个丢了) |
关键在最后一行。一个列表里只有第一项的编号能决定 start,中间行和末行的数字既不写进 start,也不落到某条 <li> 的 value 上,于是 9 和 30 就这么消失了——它不报错,只是安静地按顺序续号。我原来那句「为什么 9. 变成了 3.」,答案就卡在这一条 start 判据上。前导零那条更隐蔽:01. 经 parseInt 被当成正整数 1,判定「等于 1」后直接不写 start,连特意留的两位数都保不住。圆括号写法 1) 会一起进 <ol>,和点号混排(1. 甲 后接 2) 乙)被认成同一条列表,不另起,也不吐出一个 type 属性。
图 1:三行编号,输出只留 start 为 2
二、五种缩进写法,只嵌套出一层
大纲贴过来还带缩进,我想验证它能不能还原「甲下面挂乙」的两层结构。把 2 空格、3 空格、4 空格、Tab,以及「空格加 Tab」这五种写法各测了一遍,结果五种全部只嵌套出 1 层 <ul>。原因藏在归一化那一步:解析前先把每个 Tab 换成 4 个空格,再按父项内容列去比缩进,子项缩进只要不小于父项内容起点,就归到同一层递归里。所以「到底空了几格」这种会分出层级的事,在这里被压平成一视同仁;真想某一行变成代码而不是列表,反倒要顶格空够 4 格。还有一处容易混:- 甲 换成 * 乙 会另起列表,但有序里 1. 到 2) 却不断开。
三、标题锚点:中文留住,全角标点丢掉,同名往后加
第二件保真问题是标题的 id。锚点开关默认开着,生成 id 走的是这页自写的一小段 slug 规则,我把几种典型标题喂进去:中文标题原样保留;中英混排的 Hello 世界 被转成 hello-世界,英文转小写、空格变连字符;关键差别是全角标点——标题:副标题 里那个全角冒号被整个丢掉,成品是 标题副标题,重要,注意 同理变成 重要注意。ASCII 标点也一并删,纯符号标题最后退回兜底词 section。
同名标题的处理让我放心了一点:重复出现的标题依次追加 -1、-2、-3 后缀,从来不是覆盖前一个。我拿一份混了重复、中英混排、纯符号的文档,正文 7 个标题,把开「生成目录」时目录里的 href 和正文里写入的 id 逐个对,结果对不上 0 个——因为目录 href 与标题 id 是同一次 slug 调用的产物,结构上必然一致。用 字数统计 那类工具数长文里有多少标题时,这套后缀不改变标题条数,只是让 id 互不撞车;真要横向比字符,那是另一回事。
四、表格对齐、任务列表、统计栏与下载产物
顺带把剩下几处「没讲过」的口径量了一遍。表格分隔行里的冒号决定对齐:---: 和 :---: 会拿到 text-align 的 style,而左对齐那列 :--- 不产出任何 style 属性,全站也从不写 align 属性;把 GFM 扩展一关,整张表退回一个 <p>,竖线原样留在文本里。任务列表的方括号认小写 x 也认大写 X,开 GFM 时两者都渲染成勾选且禁用的 input;关掉 GFM 后 [ ]、[x]、[X] 这些标记就只是普通文本,既没有 class 也没有 input。
统计栏那三栏口径互不相通,是最容易归错因的一处。「字符」这一栏显示的是输入源码的 UTF-16 码元个数,不是字节、也不是字素。我拿一句 你好,世界 part_01 去对照:字符栏 13,它真正的 UTF-8 字节是 23,差了 10;渲染成 <p> 后生成的 HTML 计 30 字节。换成纯全角样例 测试:全角标点,混合。,码元只有 11,UTF-8 字节却到 33,差 22——一个汉字算一个码元却占三字节,混着中英时这个差最明显。想核对真实字节,得另拿一把 UTF-8 的尺,别信这个「字符」。
下载产物也有两处默认值值得记:整页 HTML 默认不带 BOM,产物头 4 个字节是 3c21444f(也就是 <!DO 开头),只有在编码下拉里主动选「UTF-8 + BOM」才会前置那三字节 BOM。自动转换也不是每敲一个字就重跑,而是输入停顿 150 毫秒才触发一次;页面单次处理,没有批量粘贴入口。至于示例文档那整页里含 2 个 <h1>——那是它自己的标题结构,和这里写正文无关。
五、三个绕不开的小问题
Q:为什么 9. 和 30. 的编号直接没了,也不给个提示?
它只在第一项不等于 1 时写一个 start,中间行的数字既不写进 start、也不挂到某条 li 上,所以 9、30 被按顺序续号覆盖。这页对这种「丢了编号」不告警,得自己盯住第一行的数字。
Q:统计栏的「字符」能当字节数用吗?
不能。它是 UTF-16 码元数:中文、全角标点都算 1 个码元,可一个 emoji 会被数成 2 个。要按字节核对得另走 UTF-8 那把尺,两者混着中英时差能到 22 甚至更多。
Q:缩进到底空几格才另起一层?
这页的答案是「都不另起」:2、3、4 空格、Tab、空格加 Tab 五种实测都只嵌套出 1 层,因为归一化先把 Tab 展成 4 空格,再按父项内容列去判。想要层级,得靠正文里显式的列表嵌套,而不是靠数空格。
回过头看,这页在「编号从哪来」和「id 怎么长」上,走的是自定的、可复现的小规则,而不是照抄某份规范的默认动作。原生标签透传那一头,站内早有一篇 把边界量过的旧文,两篇各管一段。