一、三級大綱貼進去,編號從第一個數一路續下去
上週整理一份 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 怎麼長」上,走的是自定的、可重現的小規則,而不是照抄某份規範的默認動作。原生標籤透傳那一頭,站內早有一篇 把邊界量過的舊文,兩篇各管一段。