少了 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 碼元。