雜記 › 技術筆記 › 把 3. 起步貼進 Markdown 轉 HTML,為什麼有序列表的編號只認第一個數

把 3. 起步貼進 Markdown 轉 HTML,為什麼有序列表的編號只認第一個數

有序列表只認首項編號,9 與 30 會被丟棄;標題錨點留中文、丟全角標點、同名追加後綴;統計欄「字符」是 UTF-16 碼元不是字節

一、三級大綱貼進去,編號從第一個數一路續下去

上週整理一份 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 屬性。

有序列表只認第一個數輸入三行2. 甲9. 乙30. 丙輸出ol start=2僅第一項被保留9 與 30 被丟棄

圖 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 怎麼長」上,走的是自定的、可重現的小規則,而不是照抄某份規範的默認動作。原生標籤透傳那一頭,站內早有一篇 把邊界量過的舊文,兩篇各管一段。

📎 相關文章

貼上 100 行分數,第 45 行被悄悄算成 1/4:約分這頁的負號、空行和大數各歸誰管 2026-11-03 文字切成 43 片再拼回去少了 42 個位元組,切口其實都落在換行上 2026-11-03 Crontab 表達式 0 0 13 * 5 到底哪天跑:這一頁只排 2 次,真實 cron 是 62 次 2026-11-03 16 組開關實測 HTML 去標籤:報 19 個標籤,實際刪掉 148 個字元 2026-10-30 SVG 轉 PNG 後外部引用的圖丟了一半:同一份 594 B 檔案預覽有 3 張圖、成品只剩 1 張 2026-10-30