生活中有些事物能同時帶給我極大的喜悅與憎恨。就像吃起來會痛的巧克力,還有 Markdown。說真的,為什麼?很多時候我們甚至沒有用到它的完整語法!
我知道你聽過有人說,他們唯一懂的程式語言是 HTML。我也知道,我們都曾不滿地翻白眼,試圖從一堆關於 HTML 只是標記語言而非程式語言的論文中,找出我們需要的資訊。
我的意思是,是的,我們是對的,但那個人可能擁有我們所沒有的東西。
(備註)當我談論 Markdown 時,我特別指的是 CommonMark,除非另有說明。因為它是明確的語法規範。我熱愛這個專案,我非常欣賞他們讓這種語言更為紮實的努力。問題不在於規格本身,而在於語言本身。
Markdown 是一種用於排版瑣碎文件的極簡語言。它只需要做一件簡單的事:接收一個 Markdown 文件,然後輸出一個 HTML 文件。它的語法極易閱讀,即使沒有任何輔助也很容易編寫。就像 C 語言一樣,你可以看到將要產生的輸出。粗體永遠是 `<b></b>`,斜體也是如此。
如果你只是個偶爾使用的使用者,學習曲線幾乎不存在。看看cheat sheet 就準備好了。
我們想要 UI 嗎?我們想要程式語言嗎?我們不知道。功能蔓延的唯一原因是規格不夠清晰。
你想要一個極簡、易讀的標記語言,你有 Markdown。就這麼簡單,對吧?
# Hello *I am an* __Unambiguous__ > Grammar
<h1>Hello</h1>
<p><em>I am an</em> <strong>Unambiguous</strong></p>
<blockquote>
<p>Grammar</p>
</blockquote>
Hello
=====
_I am an_ **Unambiguous**
> Grammar
<h1>Hello</h1>
<p><em>I am an</em> <strong>Unambigious</strong></p>
<blockquote>
<p>Grammar</p>
</blockquote>
我希望你有兩隻眼睛足夠看到 Markdown 並不是你想要的。這兩種寫法產生了完全相同的輸出。而這僅僅是冰山一角?
它內建了太多糟糕的設計,如果你試圖使用它,一旦你認為自己知道在做什麼,它就會積極地與你對抗。
而且,請不要讓我開始談論像這樣的層疊式語法:
這個東西實際上是如此頂尖,以至於我們有了一類稱為 ReDoS(Regular Expression Denial of Service,正規表達式阻斷服務)的解析器漏洞影響著它。就像這個針對 markdown-it 的 6.9(nice)嚴重等級的 CVE。
「markdown-it」是最完善、乾淨且易於理解的 Markdown 函式庫之一。我非常喜歡 markdown-it。即使是這個函式庫也受到影響,這顯示了情況有多麼糟糕。
在舊語言中,編譯器產生最佳化程式碼就像沙漠中的河流。內嵌組合語言幫助他們輕鬆編寫效能關鍵程式碼,但付出的代價是編譯器工程師的血、汗、淚,以及他們長子的誕生。
它允許在編譯器支援之前就實現像 SIMD 操作這樣的事情。如果你想了解早期 SIMD 生成失敗的概況,可以看看這裡。
現在,讓我們把這個絕妙的想法直接植入最臃腫、單執行緒、沙盒化的環境中,期望能以簡單易行的方式撰寫文件。這就是 Markdown 中內嵌 HTML 的誕生方式!
內嵌 HTML 允許你做一些事情,例如:
這不是很簡單嗎!這不是很巧妙嗎!正確解析 Markdown 之所以異常困難,並不是因為 Markdown 語法難以理解。這只佔問題的十分之一。真正問題在於,要發布一個 Markdown 解析器,你還需要發布一個友善的 HTML 解析器。而且,如果你在 Markdown 中使用 HTML,為什麼一開始不直接使用 HTML 呢!
這是寫這篇文章的人說的,他使用了所有已知的、但不在標準中的花俏功能。
Markdown 本身不足以滿足像我這樣簡單的、只在網站看起來「夠好™」時才滿意的開發者。
在這裡,「夠好™」意味著它至少需要支援基本的 $\LaTeX$ 和 Tikz,並能安裝套件、PlantUML、Mermaid、自訂樣式、自訂短代碼、標籤和分類法、正確的註腳、Bibtex 支援……
我也不想讓這個簡單的工具做簡單的工作。要將一幅畫釘在牆上,我需要一把錘子。在這裡,錘子就是 Markdown。但如果我也想畫畫,當我試圖用錘子畫畫時,我會把畫布弄破。
弄破畫布也意味著大量的 CVE,主要是圍繞著 XSS 漏洞。
內嵌 HTML 相關 CVE ▼ CVE‑2025‑24981 (XSS 漏洞) CVE‑2025‑46734 (XSS 漏洞) CVE‑2025‑7969 (XSS 漏洞) CVE‑2025‑60312 (XSS 漏洞) 每次我們允許內嵌 HTML、外掛鉤子或嵌入式執行引擎時,我們都會擴大攻擊面。
結果是可以預見的:主要的 Markdown 實作中不斷出現 XSS 漏洞。以及更多更多。隨著市場上充斥著糟糕的解析器,這將持續增加。
Markdown 和世界上所有好的技術一樣,誕生於美好的 00 年代!Markdown 的靈感來自於電子郵件和 Usenet 貼文中標記純文本的現有慣例(參考)。
請注意,在 2000 年代之前,大多數嚴肅的郵件看起來是這樣的:
你看到它的美感了嗎?引用語法、用管道字元(|)進行垂直分隔、80 行長度限制。這就是 Markdown 所需要的(但它會慘敗,因為在 Markdown 中,你無法在沒有內嵌 HTML 魔法的情況下將螢幕分成 N 部分)。
但由於這種歷史遺留問題,你有兩種寫標題的方式,正常方式和 ATX 方式。你有兩種寫粗體和斜體的方式,還有更多 CVE。你有兩種寫水平線的方式,其中一種與 setext 標題語法衝突。你有兩種寫無序列表的方式。你有一個有序列表,它不在乎你如何排序。你還有一個註腳語法,將整個語法提升到依賴上下文的語法。
這種語言簡直就是標記語言界的 C++。幾乎所有事情都可以用兩種不同的方式完成,其中一些方式可能允許 XSS 並以某種方式洩漏 HTML 中的記憶體。
它無處不在,但它也處處失敗,而且程度相同。
如果你還記得,我曾說過註腳語法將語法提升到依賴上下文的層次。讓我更詳細地闡述一下。
test [ ^1 ]
<p>test[^1]</p>
test[^1] [^1]: hello
<p>test <a href="hello">^1</a></p>
*格式化 & 我不得不將 " 改為 ″ 是出於技術原因*
實際上,CommonMark 並不支援註腳,它們有連結。唯一顯著的區別是,連結語法意味著你只能在定義後放一個詞,而不是對為什麼香蕉披薩是個好主意進行羞恥的解釋。
參考式連結和註腳需要全域定義解析。一個標記的意義取決於文件中其他地方的聲明。這打破了純粹的上下文無關語法分析假設。因此,更新到 CSG 而非 CFG。
如果你想要一個簡單的語言,就保持簡單。
我認為這是這次抱怨中最具爭議的部分。因為沒有人願意承認我們實際使用和需要的東西是截然不同的。
純粹的 Markdown 需要一個轉譯器,它實際上是一個 1:1 的映射函數。你看到 **粗體**,輸出 <b>粗體</b>。
但現代 Markdown 必須支援像註腳這樣的東西,這將這個簡單的轉譯器變成了一個完整的編譯器。在你完成這一步之後,這就是你將要攀登的階梯。
我需要一個個人知識管理系統
(* 類似 tera 模板 / 短代碼)
我發誓,我在 Obsidian 中為了滿足我的需求而使用 Markdown 所做的事情,讓我覺得 Notion 在內部複製 Word / Excel 時做出了明智的決定。
如果你問一個擁有 30 多年 8086 組語經驗的、有著雄偉鬍鬚的傢伙,答案是純文本。
如果你問一個每天創辦一家新創公司、在三台 Mac mini 上運行著 openclaw 的 Macbook 使用者,答案是 mdx。
如果你問一個以 C 語言為生的人,答案是 ReStructured Text (.rst)。
如果問我。答案是沒有。所有這些都有各自的缺陷。純文本很美,但我無法向一個不知道什麼是空指標解引用的人展示它。ReStructured Text 在你只讀它而不寫它時非常棒。而 mdx 忙於模仿 HTML,卻忘記了它需要易讀。
而所有這些最大的問題是,它們沒有建構系統。我們為了速度而走了捷徑。如果它們有建構系統,一個健全、明確、易讀的語法,專為所需而設計。我認為所有問題都會得到解決。
不允許內嵌 HTML,允許定義良好的短代碼和函數來操作文本。
允許在編譯之前、期間和之後執行自訂鉤子。
最重要的是,定義一切。我們需要的是一個客製化的工具,而不是一個我們膽敢稱之為語言的怪物。
我並不是說 Markdown / Markup 應該成為一種程式語言;我說的是我們試圖像使用程式語言一樣使用它,而由於它缺乏正式的基礎,它在這兩方面都失敗了。
我真心認為,圍繞著一個更健全的標記語言,並支援編譯時鉤子的適當建構系統,可以在正確的限制下解決許多問題。 at this point we should just let go of Markdown for good and look for our answers elsewhere. Preferably with a trivially parsable grammar.
如果你理解了我所說的,可以跳過這部分。
從形式語言理論的角度來看:
標記語言,一種標準的文本編碼系統,由一組插入到文檔中的符號組成,用於控制其結構、格式或各部分之間的關係……
電腦程式語言,用於表達一組詳細指令給數位電腦的任何一種語言……
正如你清楚看到的。Britannica 的定義不是辦法。所以我將使用我一生中一直信任的另一種東西。那就是常識,並將程式語言定義為:
如果一個語言是圖靈完備的,那麼它就是一種程式語言。
對於語言的定義,我採用了唯一的事實來源,即 1956 年的「喬姆斯基框架」。
語言:一組字串(符號的有限序列)在有限的字母表上。
$$L \subseteq \Sigma^* $$
總結來說,字母表($\Sigma$)基本上是一個包含你能想像到的所有字母的集合。或者實際上是任何你可以放入 UNICODE 而不破壞你偏好的作業系統解析器的東西。
$$ egin{align} \Sigma &= \lbrace a,b brace
ewline \Sigma_{Unicode} &= \lbrace char | char \in ext{Unicode Standard} brace \end{align} $$
而任何語言 $L$ 都是由 $\Sigma$ 中的符號組成的字串集合。
$$ L = \lbrace klasfdushsda,ksadhf, … brace $$
在語言的食物金字塔中,上下文無關語言是我們的穀物,正規語言是我們的蛋白質。正規語言是你的基本正規表達式可以解析的。上下文無關語言是 BNF/EBNF 可以描述的,而上下文相關語言是我們稱之為 C++ 語法的。
遞迴可列語言很奇怪,而且超出了這次咖啡因驅動的部落格抱怨的主題。但為了引起你的興趣。這在技術上是一個有效的 Type-0 語言。
$$ HALT = \lbrace (P, x) | P exttt{ 在輸入 } x exttt{ 上停止 } brace $$
這就引出了我們的第二點。
圖靈完備性:能夠計算所有圖靈可計算函數的計算系統稱為圖靈完備(或圖靈強大)。或者說,這樣的系統能夠模擬通用圖靈機。(維基百科)
圖靈可計算函數,用通俗的話來說,就是一個函數,它要麼返回一個值,要麼在嘗試過程中失敗(即停止),無論是在有限還是無限的時間內。
為什麼要進行這個關於程式語言/語言分類的 CS 101 課程?因為我想讓你理解,簡單的「願望」可能會導致巨大的改變。
將一組數學公理與可證性與不可證性區分開來的是對遞迴(自我參照)的渴望,這表現為常見的乘法運算。一個微不足道的運算可能產生非同尋常的反應。
如果你喜歡我的閒聊,你也可以看看這些文章!在實際寫這篇文章之前,我並沒有讀過它們。我對 Markdown 的經歷是完全獨立的。我在論壇和主要是 Obsidian 中使用它。我的想法是我自己的。我不支持任何人,但我確實認為有些人比其他人更正確。
A deep-dive into the history, decisions, controversies, and personalities b…
返回 BGs Labs 的主頁面。