《GitHub 風格的 Markdown 正式規範》發布
五年前,我們在 Sundown 的基礎之上開始構建 GitHub 自定義版本的 Markdown —— GFM ( GitHub 風格的 Markdown ),這是我們特地為解決當時已有的 Markdown 解析器的不足而開發的一款解析器。
今天,我們希望通過發布 GitHub 風格的 Markdown 的正式語法規範及其相應的參考實現來改善現狀。
該正式規範基於 CommonMark,這是一個雄心勃勃的項目,旨在通過一個反映現實世界用法的方式來規範目前互聯網上絕大多數網站使用的 Markdown 語法。CommonMark 允許人們以他們原有的習慣來使用 Markdown,同時為開發者提供一個綜合規範和參考實例,從而實現跨平台的 Markdown 互操作和顯示。
規範
使用 CommonMark 規範並圍繞它來重新加工我們當前用戶內容需要不少努力。我們糾結的主要問題是該規範 (及其參考實現) 過多關注由原生 Perl 實現支持的 Markdown 通用子集。這還不包括那些 GitHub 上已經在用的擴展特性。最明顯的就是缺少 表格 、 刪除線 、 自動鏈接 和 任務列表 的支持。
為完全描述 GitHub 的 Markdown 版本 (也稱為 GFM),我們必須要要正式定義這些特性的的語法和語意,這在以前從未做過。我們是在現存的 CommonMark 規範中來完成這一項工作的,同時還特意關注以確保我們的擴展是原有規範的一個嚴格且可選的超集。
當評估 GFM 規範 的時候,你可以清楚的知道哪些是 GFM 特定規範的補充內容,因為它們都高亮顯示了。並且你也會看到原有規範的所有部分都保持原樣,因此,GFM 規範能夠與任何其他的實現保持兼容。
實現
為確保我們網站中的 Markdown 渲染能夠完美兼容 CommonMark 規範,GitHub 的 GFM 解析器的後端實現基於 cmark
來開發,這是 CommonMark 規範的一個參考實現,由 John MacFarlane 和許多其他的 出色的貢獻者 開發完成。
就像規範本身那樣,cmark
是 Markdown 的嚴格子集解析器,所以我們還必須在現存解析器的基礎上完成 GitHub 自定義擴展的解析功能。你可以通過 cmark
的分支 來查看變更記錄;為了跟蹤不斷改進的上游項目,我們持續將我們的補丁變基到上游主線上去。我們希望,這些擴展的正式規範一旦確定,這些補丁集同樣可以應用到原始項目的上游變更中去。
除了在 cmark
分支中實現 GFM 規範特性,我們也同時將許多目標相似的變更貢獻到上游。絕大多數的貢獻都主要圍繞性能和安全。我們的後端每天都需要渲染大量的 Markdown 文檔,所以我們主要關注這些操作可以儘可能的高效率完成,同時還要確保那些濫用的惡意 Markdown 文檔無法攻擊到我們的伺服器。
第一版使用 C 語言編寫的解析器存在嚴重的安全隱患:通過足夠深度的特殊 Markdown 元素的嵌套,它可能造成堆棧溢出 (甚至有時候可以運行任意代碼)。而 cmark
實現,就像我們之前設計的解析器 Sundown,從一開始設計就考慮到要抵禦這些攻擊。其解析演算法和基於 AST 的輸出可以優雅的解決深層遞歸以及其他的惡意文檔格式。
cmark
在性能方面則是有點粗糙:基於實現 Sundown 時我們所學到的性能技巧,我們向上游貢獻了許多優化方案,但除去所有這些變更之外,當前版本的 cmark
仍然無法與 Sundown 本身匹敵:我們的基準測試表明,cmark
在絕大多數文檔渲染的性能上要比 Sundown 低 20% 到 30%。
那句古老的優化諺語 最快的代碼就是不需要運行的代碼 在此處同樣適用:實際上,cmark
比 Sundown 要多進行一些操作。在其他的功能上,cmark
支持 UTF8 字符集,對參考的支持、擴展的介面清理的效果更佳。最重要的是它如同 Sundown 那樣,並不會將 Markdown 翻譯成 HTML。它實際上從 Markdown 源碼中生成一個 AST (抽象語法樹,Abstract Syntax Tree),然後我們就看將之轉換和逐漸渲染成 HTML。
如果考慮下我們在 Sundown 的最初實現 (特別是文檔中關於查詢用戶的 mention 和 issue 引用、插入任務列表等) 時的 HTML 語法剖析工作量,你會發現 cmark
基於 AST 的方法可以節約大量時間 和 降低我們用戶內容堆棧的複雜度。Markdown AST 是一個非常強大的工具,並且值得 cmark
生成它所付出的性能成本。
遷移
變更我們用戶的內容堆棧以兼容 CommonMark 規範,並不同於轉換我們用來解析 Markdown 的庫那樣容易:目前我們在遇到最根本的障礙就是由於一些不常用語法 (LCTT 譯註:原文是 the Corner,作為名詞的原意為角落、偏僻處、窘境,這應該是指那些不常用語法),CommonMark 規範 (以及有歧義的 Markdown 原文) 可能會以一種意想不到的方式來渲染一些老舊的 Markdown 內容。
通過綜合分析 GitHub 中大量的 Markdown 語料庫,我們斷定現存的用戶內容只有不到 1% 會受到新版本實現的影響:我們是通過同時使用新 (cmark
,兼容 CommonMark 規範) 舊 (Sundown) 版本的庫來渲染大量的 Markdown 文檔、標準化 HTML 結果、分析它們的不同點,最後才得到這一個數據的。
只有 1% 的文檔存在少量的渲染問題,使得換用新實現並獲取其更多出看起來是非常合理的權衡,但是是根據當前 GitHub 的規模,這個 1% 是非常多的內容以及很多的受影響用戶。我們真的不想導致任何用戶需要重新校對一個老舊的問題、看到先前可以渲染成 HTML 的內容又呈現為 ASCII 碼 —— 儘管這明顯不會導致任何原始內容的丟失,卻是糟糕的用戶體驗。
因此,我們想出相應的方法來緩和遷移過程。首先,第一件我們做的事就是收集用戶託管在我們網站上的兩種不同類型 Markdown 的數據:用戶的評論 (比如 Gist、issue、PR 等)以及在 git 倉庫中的 Markdown 文檔。
這兩種內容有著本質上的區別:用戶評論存儲在我們的資料庫中,這意味著他們的 Markdown 語法可以標準化 (比如添加或移除空格、修正縮進或則插入缺失的 Markdown 說明符,直到它們可正常渲染為止)。然而,那些存儲在 Git 倉庫中的 Markdown 文檔則是 根本 無法觸及,因為這些內容已經散列成為 Git 存儲模型的一部分。
幸運的是,我們發現絕大多數使用了複雜的 Markdown 特性的用戶內容都是用戶評論 (特別是 issue 主體和 PR 主體),而存儲於倉庫中的文檔則大多數情況下都可以使用新的和舊的渲染器正常進行渲染。
因此,我們加快了標準化現存用戶內容的語法的進程,以便使它們在新舊實現下渲染效果一致。
我們用以文檔轉換的方法相當實用:我們那箇舊的 Markdown 解析器, Sundown,更多的是扮演著翻譯器而非解析器的角色。輸入 Markdown 內容之後,一系列的語意回調就會把原始的 Markdown 內容轉換為目標語言 (在我們的實際使用中是 HTML5) 的對應標記。基於這一設計方法,我們決定使用語意回調讓 Sumdown 將原始 Markdown 轉換為兼容 CommonMark 的 Markdown,而非 HTML。
除了轉換之外,這還是一個高效的標準化過程,並且我們對此信心滿滿,畢竟完成這一任務的是我們在五年前就使用過的解析器。因此,所有的現存文檔在保留其原始語意的情況下都能夠進行明確的解析。
一旦升級 Sundown 來標準化輸入文檔並充分測試之後,我們就會做好開啟轉換進程的準備。最開始的一步,就是對所有新用戶內容切換到新的 cmark
實現上,以便確保我們能有一個有限的分界點來進行過渡。實際上,幾個月前我們就為網站上所有 新的 用戶評論啟用了 CommonMark,這一過程幾乎沒有引起任何人注意 —— 這是關於 CommonMark 團隊出色工作的證明,通過一個最具現實世界用法的方式來正式規範 Markdown 語言。
在後端,我們開啟 MySQL 轉換來升級替代所有 Markdown 用戶內容。在所有的評論進行標準化之後,在將其寫回到資料庫之前,我們將使用新實現來進行渲染並與舊實現的渲染結果進行對比,以確保 HTML 輸出結果視覺上感覺相同,並且用戶數據在任何情況下都不會被破壞。總而言之,只有不到 1% 的輸入文檔會受到標準進程的修改,這符合我們的的期望,同時再次證明 CommonMark 規範能夠呈現語言的真實用法。
整個過程會持續好幾天,最後的結果是網站上所有的 Markdown 用戶內容會得到全面升級以符合新的 Markdown 標準,同時確保所有的最終渲染輸出效果對用戶視覺上感覺相同。
結論
從今天 (LCTT 譯註:原文發佈於 2017 年 3 月 14 日,這裡的今天應該是這個日期) 開始, 我們同樣為所有存儲在 Git 倉庫中的 Markdown 內容啟動 CommonMark 渲染。正如上文所述,所有的現存文檔都不會進行標準化,因為我們所期望中的多數渲染效果都剛剛好。
能夠讓在 GitHub 上的所有 Markdown 內容符合一個動態變化且使用的標準,同時還可以為我的用戶提供一個關於 GFM 如何進行解析和渲染 清晰且權威的參考說明,我們是相當激動的。
我們還將致力於 CommonMark 規範,一直到在它正式發布之前消除最後一個 bug。我們也希望 GitHub.com 在其 1.0 規範發布之後可以進行完美兼容。
作為結束,以下為想要學習 CommonMark 規範或則自己來編寫實現的朋友提供一些有用的鏈接。
- CommonMark 主頁,可以了解本項目更多信息
- CommonMark 論壇討論區,可以提出關於該規範的的問題和更改建議
- CommonMark 規範
- 使用 C 語言編寫的參考實現
- Our fork with support for all GFM extensions
- GFM 規範,基於原始規範
- 使用其他編程語言編寫的 CommonMark 實現列表
譯者簡介:
GHLandy —— 生活中所有歡樂與苦悶都應藏在心中,有些事兒註定無人知曉,自己也無從說起。
via: https://githubengineering.com/a-formal-spec-for-github-markdown/
作者:Yuki IzumiVicent Martí 譯者:GHLandy 校對:jasminepeng
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive