研究 Markdown 顯示不一致的歷程

黑盒子底下沒有說的秘密

事件的起因是我在進度報告使用了以下的語法

* test
blah1

預期的結果:

  • test
    blah1

但實際結果卻是:
no

十分醜陋的段落岔了出來,之前寫筆記的時候也有發現這個狀況,但今天就特別好奇這背後的原因是什麼?為此我把上面那段語法丟到 hackMD 上測試,結果為前者,我不知道原因是什麼只好詢問老師。後來老師說學習系統的 markdown 語法是 GitHub Flavored Markdown (GFM) 然後說學習系統有開源程式碼可以自己研究看看唷。於是我就展開了今天的不歸路

迅速展開的技術細節 - 複雜度上升

花時間消化文件的內容。學習系統採用的框架是 react,然後是透過 react-markdown 的套件進行 markdown 語法的處理。react-markdown 遵循的 markdown 標準為 CommonMarkGitHub Flavored Markdown (GFM) 完整繼承了 CommonMark 語法,同時又增加了刪除線、表格、TODO list、直接URL等語法功能。

react-markdown 經過了好幾道步驟把 markdown 語法轉換成 html 語言。那時候我只是想要知道解析語法的規則,要的只是「喔~原來是經過這串程式碼處理變這成那個樣子阿」。

這邊只是單純想 trace code,不是為了串工具或開發新功能,純粹想研究而已。
可能套件有提供解析參數可以設定,可能套件可以讓我開發自己的markdown插件,但這些都要 trace 過才會知道狀況。
疑惑是因為不理解,等到理解之後就不會有問題了

線上測試工具並非實際環境 - 換行字元變空白

中間看到套件庫的文件有提供 live demo tool,想說可以測試研究看看的。殊不知,線上版本的編輯器真的就只是轉換成一般的 html ,然後為了搞清楚「html 內的換行字元預設沒效果,只有 <br> 才能看到換行」這件事情,當下又折騰了不少時間。

後來查資料建議用 <pre> 或是使用 CSS 的 white-space 設定排版,參照 [[CommonMark#附錄A CSS white-space 設定]]

問題原因:
CommonMark 規範內有提到 Hard line breaks 和 Soft line breaks 兩種方式。

  • Hard line breaks 會被處理成 <br>
  • Soft line breaks 會被處理成 softbreak
    • 解析器可以設定轉換成 line break 或 space
    • 渲染器也提供把 softbreak 轉換成 Hard line breaks 的選項

爭端 - 缺乏明確規範的 markdown

  • 1.2Why is a spec needed?
    說明為何需要一個規範?在非常多的狀況下未明確定義導致分歧的實作

原始的 markdown.pl是一種非正式規範,隨著語法流行,大家各自實作的標準不一致,造成不同平台的 markdown 呈現的結果不同。於是一群開發者發起了標準化的工作,而 CommonMark 就是尋求標準化的產物,目前還存在一些問題沒辦法正式發布 1.0 版。

看起來簡單的東西背後其實並不簡單

  • List items
    CommonMark 規範花最多篇幅說明的地方,總共有五個處理的準則。
  • 5.2.1Motivation
    說明原始的 markdown.pl 的 four-space rule 以及實作上遇到的問題

列表項目的時候,不得不說規格書講得很仔細,但我一樣看得有點模糊。但經過這段經驗讓我曉得程式在背後是依照什麼樣子規則在解析這些語法?一般的 Markdown 語法教學文件給的 case 是比較簡單的,可是規格書就會給出更多的 case,這是為了確保大家各自實作出來的版本都可以跟規格相符。

再回頭試著解釋當初遇到的問題

有了前面的背景知識之後,嘗試解讀一開始 rendering 不一致的案例中發生了什麼事情?

* test
blahblah

* test2
  foofoofoo

在進度報告呈現出來的結果,看到 blahblah 被當成段落,然後 test 和 test2 被當成兩個獨立的清單。也就是說分離太遠的 list 會被認為是兩個 ul ,另外因為設定了CSS white-space: pre-wrap 的關係,會正常換行。

<div class="md-body">
  <ul>
    <li>test</li>
  </ul>
  <p>blahblah</p>
  <ul>
    <li>test2
foofoofoo</li>
  </ul>
</div>

在 commonmark.js 的 live demo test blahblah 被認為是同個段落,上下兩個 li 被認定是同個 ul 。

<ul>
  <li>
    <p>test
    blahblah</p>
  </li>
  <li>
    <p>test2
    blahblahblah</p>
  </li>
</ul>

轉換成 AST 語法樹的樣子,看到 list 有一個額外屬性是 tight 可以用來判斷是列表間隔是寬鬆還是緊密,然後換行字元被處理成 softbreak,render 可以決定要換成換行還是空白?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document SYSTEM "CommonMark.dtd">

<document xmlns="http://commonmark.org/xml/1.0">
  <list type="bullet" tight="false">
    <item>
      <paragraph>
        <text>test</text>
        <softbreak />
        <text>blahblah</text>
      </paragraph>
    </item>
    <item>
      <paragraph>
        <text>test2</text>
        <softbreak />
        <text>blahblahblah</text>
      </paragraph>
    </item>
  </list>
</document>

HackMD 的轉換結果,靠 markdown-it 套件完成。看得出來是把 softbreak 換成 <br> 段落縮排的判定比較沒那麼嚴謹。值得一提的是上面有提供 linter 會跳出語法錯誤

<ul>
<li>
<p>test<br>
blahblah</p>
</li>
<li>
<p>test2<br>
foofoofoo</p>
</li>
</ul>

比較:

  • react-markdown (學習系統): 要縮排不接受省略,換行使用 pre-wrap
  • commonmark.js: 自由風格可縮可不縮,li 內包含段落,接受寬鬆風格 list,不處理換行
  • markdown-it (HackMD): 自由風格可縮可不縮,li 內包含段落,換行使用 <br>

推測產生不一致的原因:parsing 方式不同,需要比對中間過程產出的 AST 才知道
測試方法:用規格書中提供的 example 當測試範例去比對是否符合規範?應該存在相關的一致性測試工具
不一致也代表這樣的寫法可能不是好的格式,可讀性不佳,閱讀時容易誤解之類的。

救星登場 - linter

MD032 - Lists should be surrounded by blank lines
這個規則希望 list 項目和單行文字分開,除了美觀以外,也是因為有些解析器不處理的關係。後面有說後面的單行文字如果有縮排是可以接受的

MD005 - Inconsistent indentation for list items at the same level
MD007 - Unordered list indentation
一些跟縮排有關的規則,這邊是不同 markdown 實作最容易發生分歧的地方。4個空白字元縮排是目前最沒有爭議的縮排方式。在其他地方顯示也比較不會發生分歧

這些 linter 創造出來的本意並不是限制大家使用的語法,而是提供一個可以讓大家共同參照的規則樣式。這些規則其實都可以根據自己的需要調整並且發布為規則樣式文件。

附錄A: CSS white-space 設定

  • normal 預設。忽略空白
  • pre 保留空白。行為類似 <pre>
  • nowrap 不處理換行,直到遇到 <br>
  • pre-wrap 保留空白序列,正常換行
  • pre-line 合併空白序列,保留換行
  • inherit 繼承父元素

原文:https://blog.csdn.net/qq_36370731/article/details/78069056
授權:CC 4.0 BY-SA

附錄B: 致謝

感謝 Aaron Swartz 參與設計 markdown 的過程中提供各式各樣驚奇的想法和回饋,並且提供了 html2text 這項便利工具將 html 轉換成 markdown 格式。願他在天上幸福

Select a repo