少了 5 个码元,界面一个字都没提
先是一笔小账:把一段旧日志粘进转换页,源文 11 个码元,渲染只读到 6 个,少掉的 5 个在预览和成品里都找不到痕迹,mdWarnings 是空数组。另一笔是有人问:复制走的 HTML 比原文少了一截,谁吃掉的。读完这页脚本才确认,「悄悄少字」是三处各自静默的机制,标题里的 271 是其中一处的极值。
被测页是Markdown 转 HTML,模板 1135 行、41623 字节、md* 函数 72 个;测法是把函数逐字摘进纯 Node,DOM 走 mdGet 的 null 分支。
中间态埋在正文串里
这页不边解析边输出:行内代码、链接、图片、强调先算一步的结果,都换成一枚「洞」留在串里——L235、L236 两枚常量做标记,mdStashPut(L323)返回「码位 2 + 下标 + 码位 3」,真正的 HTML 存在 stash.list,块级拼完后主链只在 L668 调一次 mdUnstash 回填。
下标是入库时 list 的长度,跟文档顺序无关:# A **b** `c` 起头那篇,回填前串里的下标序列是 2,0,1,3,4,6,5,粗体的 2 反而排在行内代码的 0 前面。
64 轮上限,只有构造的链撞得上
mdUnstash(L333)的循环条件是「串里还有码位 2 且轮数没到 64」;退出后 L343 无条件删残留洞,L329 到 L345 整段没调用 mdWarn。撞上限要造洞值套洞的链(每层 = A + 内层洞 + B,最内层 Z):depth 64 时轮数恰好 64、全展开 127 码元;depth 65 起轮数锁在 64、输出锁在 128 码元,丢字符 = 2*(depth - 64) - 1,于是 65 丢 1、66 丢 3、70 丢 11、100 丢 71、200 丢 271,全程零提示。
图 1:链深与回填轮数。
这条上限跟文档长无关:mdSample() 是 53 行 / 655 码元,回填前 17 枚洞,逐轮记录只有一条「第 0 轮:洞 17 变 0」,1 轮完、删 0 字符;2000 段、97778 码元的文档有 6000 枚洞,轮数仍是 1,产物 179779 码元,探针机器耗时 58 ms。洞互不嵌套时一轮就换完,64 轮给典型文档留了 63 轮余量。
越界洞是另一条静默路径:输入 X洞0-洞7-洞2Y(list 只有 3 项),洞 7 取不到值被 L337 原样留着,第 2 轮 break,随后 L343 把它按 3 字符删掉,成品是 17 码元的 X<b>0</b>--<u>2</u>Y,合法洞照常回填。
归一化先吃字符
mdNormalize(L523–529)的 L527 删码位 0 到 4 五档:甲 + 码位 n + 乙 由 3 码元变 2,五枚各命中 1,连粘 5 枚时 7 码元变 2,U+0005、U+0006 一枚不删。开头那笔 11 码元读到 6 码元就在这一步;mdWarn 全页只有 L696 与 L766 两个调用点,所以提示 0 条。同一枚正则顺带堵住伪造:正文写「码位 2 0 码位 3」冒充洞,9 码元进来、归一化后 7 码元,标记没了、只剩正文里那个 0。
L526 把每枚 tab 恒换成 4 枚空格,档位只认 4:3 空格出 <ul>,正好 4 空格出 <pre><code>,5 空格退回 <p> - 甲</p>;tab 恒折进 4 空格档、空格加 tab 落 5 空格档、两枚 tab 的 8 空格是段落;子项缩进够 4 空格就算下一层,这层账列表编号与标题锚点算过。
提示只收 20 条
表头 2 列加 34 行 3 列的表格每行多一格,L766 逐行出一条,mdWarn 被调用 34 次,而 L498 只在前 20 条入库:数组停在 20,14 条无声消失。#md-err 文本 513 码元、按 ' | ' 恰好 20 条、末条正好是「表格第 20 行…」,第 21 到 34 行连字符都不出现。额度也不跨次累加:L660 每次渲染重置数组,连渲 5 次依次 20,20,20,20,20。
12 个 pass 的顺序真值表
mdInline(L480–495)固定 12 步,行内代码在前、软换行收尾;把相邻两步换序,28 例输入里只有 4 对在输出上看得见差别。
| 换序的那一对 | 差异 | 看到什么 |
|---|---|---|
| images 与 links | 1/28 | 括号被 links 先吃,感叹号裸在外面 |
| links 与 urls | 2/28 | 右括号被一起吞进地址 |
| escapes 与 emphasis | 1/28 | 反斜杠漏进成品 |
| rawTags 与 esc | 4/28 | 标签先被转义,兜不回来 |
| 另外 7 对 | 0/28 | 本组样本没触发到,不等于永远可交换 |
图片那例输入 ,换序后成 !<a href="…">alt <em>x</em></a>;\*不是斜体\* 那例换序后两枚反斜杠留在标签两侧。mdEmphUnderscore(L456)把汉字列进「不许相邻」,snake_case_word、_斜体_中文 洞数全 0、原样输出。
两个复制按钮差 106
66 码元那篇(含 <b>、引号、a&b、带标题链接)默认档渲染:「复制 HTML」(L1051)给 135 码元原样,「复制后转义」(L1054)再套一层实体给 241 码元。差值可逐字符对账:每枚 & 加 4、每枚 < 与 > 各加 3、每枚 " 加 5,本例 4×7 + 3×8 + 3×8 + 5×6 = 106,同式在示例产物上给出 1559 → 2565。字符集跟 mdEsc(L265)不同:它把单引号写成 ',L1055 那四步不动单引号;只补这一层实体是 HTML 转义的活。
清空再刷新,样例又回来
mdStore(L1101)只写两键:原文进 md2html-input,选项进 md2html-opts(7 字段 = 6 勾选加 charset)。mdRestore(L1117)在 store 空、opts 坏 JSON、window.localStorage 为 null、点「清空」后 store 成空串这四种情况下都回落 mdSample();第四种最反直觉:空串让 raw && raw.length 走假分支,刷回来的是 655 码元示例。
常见问题
Q:少掉的字为什么不报警?
只有两个来源,L696 的围栏未闭合与 L766 的表格超列;L527 删控制字符、L343 删残留洞、第 21 条起的截断都不在提示路径上。
Q:怎么分清是归一化吃的还是回填吃的?
少的枚数落在码位 0 到 4、且 U+0005 之后不动,是 L527;整段尾巴不见或一次少 3 字符,是 L343 删的洞。
Q:长文会不会撞 64 轮?
实测不会,97778 码元、6000 枚洞仍是 1 轮;只有洞值套洞的人造链撞得到,撞到就锁在 128 码元。