一、滿屏的字面量是從哪一步冒出來的
整理一篇內部文檔時,我在 Markdown 裡嵌了一段現成的 HTML:一個 <div class="callout"> 包住三行提醒,想靠它自帶的邊框把「這段設定別亂改」標出來。轉完以後邊框沒有,標籤倒整整齊齊排在地面上,尖括號全變成了 < 與 >。
卡住的是「允許 HTML」這個預設關著的開關:關掉時正文裡的 <b> 一律被轉義成字面量,開啟後行內與塊級標籤才原樣透傳。同一份源碼 x <b>y</b> z,關著產出 28 字元,開著產出 20 字元,差的就是被轉義撐出來的那幾個。
開關撥上去之後新的困惑跟著來了:那段示例代碼塊裡的 <div> 仍是字面量,像什麼都沒發生過。這一篇把透傳的邊界逐條講清,順帶說說表格、任務清單和標題層級上容易歸錯因的幾處。
二、透傳有三條硬邊界,差一條就退回轉義
第一條是實體要成串。源碼裡的 ©、&、A、A 在開關關閉時被整體二次轉義,螢幕上留下 © 這種前面多套一層的字樣;開啟後具名、十進位、十六進位三種成形實體一起放行。裸的 & 號與引號不在放行範圍,開著也照樣被寫成 & 和 "。
第二條是標籤形狀。透傳要求小於號後面緊跟字母或驚嘆號,並以大於號收尾。夾在段落中間的 a <!-- hidden --> b 開著也被轉義,行內那一路不接受註釋;獨佔一行的 <!-- c --> 反倒原樣落地,塊級分支的字元類別裡帶著驚嘆號。沒收尾的 <b c 同樣按普通文字處理。
第三條是屬性體的長度。屬性段正好 400 字元時 <b x="…"> 整句透傳;再加一個字元到 401,開標籤被轉義、閉標籤 </b> 仍透傳,一句 HTML 被劈成半轉義半標籤。
能碰上的上限就這兩處:屬性體 400 字元、轉換提示 20 條(第 21 條起靜默丟棄)。字數與體積沒有門檻,輸入區不做長度判斷,也沒有超限回退。
三、代碼塊這一路只經過轉義,開關碰不到它
圍欄代碼塊和 4 空格縮進代碼塊都直接進轉義那一步,不經過處理原始 HTML 的分支。於是 ```html 圍欄裡的 <div class="x">y</div>,開關開著與關著拿到逐位元組相同的輸出;縮進代碼裡貼一段腳本,兩種設定下同樣寫成轉義字樣。
行內代碼是同一口徑:一對反引號包住的 <b>,開關兩側都輸出 <code><b></code>。甚至開著「允許 HTML」之後,反引號裡的成形實體會被再多轉一層,落點比關著時更繞。
這麼一想就順了:開關回答的是「我寫的這段是不是真 HTML」,代碼塊天生在說「我在展示別人的 HTML」。我是把同一段源碼在 Markdown 轉 HTML 的兩種設定下各跑一遍、再逐字元比輸出才確認的。
四、開啟之後,反而變差的兩件事
第一件是尖括號自動連結。獨佔一行的 <https://example.org>,關著時正常出錨點;開著時塊級分支先命中,輸出原樣還是 <https://example.org> 這一段文字,連結沒了。同一行前面墊幾個字才回到行內那一路,錨點重新出現。
第二件是它根本不是過濾器。沒有標籤白名單這回事:開著時 <script>alert(1)</script> 原樣進結果,<span onclick="alert(1)"> 的事件屬性不會被洗掉,<iframe> 也照抄,而預覽那一側走的是 innerHTML,這些標籤是真會生效的。所以它的定位是「我知道自己在寫什麼」,不是「我貼的內容可以隨便信」。
五、表格、任務清單和標題:靜默的地方比提示多
表格的入場券是一行分隔行,且那一行裡同時有短橫與豎線。差一點,落點就完全不同:
| 輸入 | 實測輸出 |
|---|---|
| 表頭、分隔行、一行資料 | 206 字元,thead 與 tbody 齊全 |
| 同一張表關掉 GFM | 整張表退回一個 <p>,不出 table 標籤 |
| 把分隔行刪掉 | 同樣退回 <p>,且不給任何提示 |
| 資料行比表頭多一格 | 只出 2 個 <td>,多餘內容沒了,提示寫「表格第 1 行儲存格比表頭多」 |
:---、:---:、---: 三欄 | 只有置中與右對齊拿到 style,左對齊那欄不帶 style |
| a | 下面緊接一行 --- | 被認成底線式標題,整張表變成 <h2 id="a"> |
- 提示裡的「第 1 行」是資料行序號,不是來源檔案的行號,照著它去數原文容易錯位。
- 重新點一次轉換是把結果整體覆蓋,不會把兩輪的提示或正文疊起來。
- 任務清單
- [x]與- [ ]出<input type="checkbox" checked disabled>,每條<li>帶class="md-task",大寫 X 同樣算勾選;關掉 GFM 後方括號標記按原文留在正文裡,沒有 class 也沒有 input。
標題上有兩處容易歸錯因:七個井字號起頭不會被認成標題,層級最高只到 h6,目錄的嵌套因此嚴格跟著標題層級走、跳級會插進空的一層 <ul>。標題 id 走自建 slug——ASCII 標點整枚丟棄、中文保留、空格轉連字號、同名依次追加 -1、-2,這套並不屬於 CommonMark 或 GFM 的規定動作。
而 id 的寫入條件是「標題錨點」與「產生目錄」有一個為真:只關掉錨點、目錄還開著,正文裡照樣出現 <h1 id="a">;兩個都關才拿到不帶 id 的標籤。
六、三個繞不過去的小問題
Q:代碼塊裡的 HTML 有辦法透傳出來嗎?
沒有辦法。圍欄與縮進兩條分支只經過轉義,根本不看「允許 HTML」。想讓它真的變成結構,就寫進正文並開啟開關;只想展示代碼本身,留在代碼塊裡,字面量正是你要的結果。
Q:開著「允許 HTML」,貼來的腳本會被擋住嗎?
擋不住,它不是安全過濾,也沒有標籤白名單。帶腳本的標籤、帶事件屬性的 span、iframe 都原樣進結果,預覽那一側還會真的生效。要發到公網的稿子,我先把開關關著跑一遍,把滿地字面量當一份待審清單。
Q:統計那一行的「字元」和「HTML」為什麼對不上?
兩欄用的是不同的尺:「字元」按輸入源碼的 UTF-16 碼元數計,「HTML」按輸出文字的 UTF-8 位元組計,混著中文時差距會被拉大——一個漢字算一個碼元,卻不止一個位元組。
透傳只覆蓋正文裡成形且沒超長的標籤,代碼塊、不成串的實體、寫到一半的尖括號都在覆蓋之外。先弄清自己那段文字走的是哪一路分支,比記住開關的名字有用得多。