# CommonMark 的 flanking 規則會在日文與中文頁面印出 **

> CommonMark 只有在 right-flanking 時才讓 ** 關閉強調。日文與中文在句號後面沒有空白，於是粗體就以星號的原樣出現在頁面上。

- Source: https://oharu121.com/zh-tw/blog/commonmark-flanking-rules-cjk-bold-literal-asterisks/
- Published: 2026-09-20T21:47:52+09:00
- Tags: Markdown, 國際化

---
## 引言

我用三種語言發布了一篇文章，打開日文頁面想讀一讀，卻看到本該是粗體的那一句兩端排著星號。

```text
**SIGTERM を受け取った Chrome は、終了する際に自分の SingletonLock を削除します。**これはテスト用に…
```

原因出在 CommonMark。**`**` 這組分隔符號只有在 right-flanking 時才能關閉並強調為粗體**。夾在句號與文字之間的分隔符號，既不是 left-flanking 也不是 right-flanking。英文不會出現這種情況，因為每個句號後面都跟著一個空白。CJK （中文、日文、韓文）沒有這個空白。

## 決定一組 `**` 能不能關閉的東西

一串星號不是一道指令。**它只是個候選，兩側的那兩個字元決定這個候選可以變成什麼。**

### left-flanking 與 right-flanking

粗體像括號一樣，是由**一對**分隔符號框起來的。一組負責開啟強調，另一組負責關閉，而解析器在配對之前，會先判定每一組符合哪一種角色的資格。

兩種角色都不符合的分隔符號不是錯誤，它會以兩個星號的原樣留在頁面上。

資格是逐組判定的，判定的依據只有緊鄰的字元。**一組符號能否開啟，看的是它緊貼著它要開啟的文字；能否關閉，看的是它緊貼著它要關閉的文字**。內側只要有一個空白，這個判定就失效了。

```text
This is **bold text** here      both runs touch the text  →  renders
This is ** bold text** here     opening run has a space   →  stays literal
This is **bold text ** here     closing run has a space   →  stays literal
```

CommonMark 給這兩種資格各取了名字，並把每一種都寫成對分隔符號兩側字元的條件。

- **left-flanking**，是指*這組符號*後面不是空白，而且後面不是標點，或者前面是空白或標點。
- **right-flanking**，是指*這組符號*前面不是空白，而且前面不是標點，或者後面是空白或標點。

每一個子句的主詞都是這組符號「 `**` 」本身，而「後面」指的是緊接在它之後的那一個字元。left-flanking 是**開啟**的資格，right-flanking 是**關閉**的資格。

*Figure — EmphasisPair: 同一個句子寫成三種樣子，並為每一組符號標上說明。內側的空白只會讓其中一組失格，另一組條件仍然成立，只是失去了配對的對象。這樣就足以讓兩端的星號留在頁面上。*

**一組符號要開啟強調只能靠 left-flanking，要關閉只能靠 right-flanking**。文件裡其他東西一概無關：與段落無關，與配對的另一組也無關。真正有作用的只有前後各一個字元。

*Figure — FlankingTest: 分隔符號兩側的兩個字元決定一切。前面是標點的關閉端，後面必須是空白或標點；如果那裡出現文字就失格。*

關閉端這個情況值得慢慢讀，因為出問題的就是它。前面是標點的分隔符號要成為 right-flanking，**只有**在後面是空白或標點時才行。左邊標點、右邊文字這種排列，兩個子句都不滿足。

### 英文的句號為什麼永遠安全

英文會在句號後面寫一個空白，而這個空白是有作用的。

```text
**It removes the lock.** This is a test.
```

關閉端前面是 `.`、後面是空白，滿足 right-flanking 的第二個子句，這組符號就關得起來。**英文之所以不出問題，靠的就只是這個空白**。英文句末的粗體不必特別處理就自然是這個情況，所以用英文寫作時，根本不會察覺有這條規則。

*Figure — EnglishSpace: 同一個句子並排兩種語言。英文句號後面的空白，正是讓關閉端成為 right-flanking 的東西；日文在那個位置什麼都沒有。*

日文與中文的句子之間不放空白。關閉端因此會緊貼著下一句的第一個字元，而那個字元幾乎都是普通文字。

### 在 CJK 會失敗的三種形式

CJK 技術文章裡經常出現下面三種排列，而三種都會產生一組什麼都做不了的符號。

```text
…削除します。**これは      closing run: 。 on the left, こ on the right
…対象を**「引用」**とする    opening run: を on the left, 「 on the right
…ではなく**`client-id`**です  opening run: く on the left, ` on the right
```

第一種是粗體剛好在句末結束。第二種是用引號括起來的詞語，引號本身是標點，於是開啟端失格。第三種是用粗體包住行內程式碼，反引號起了同樣的作用。

**每一種修法都是把標點移到強調之外，而不是在內側補一個空白。**

```text
…削除します**。これは
…対象を「**引用**」とする
…ではなく **`client-id`** です
```

*Figure — FailingShapes: 三種形式與各自的修法。前兩種只是把一個字元移到強調之外；只有第三種因為反引號動不了，才退而求其次在外側加空白。*

第三種修法比較難看，因為中文字和粗體之間會看得到一個空白。但當分隔符號緊貼著一個動不了的反引號時，也只有這個辦法。

## 四次掃描，以及弄壞三個段落的那一次

手工一個個修不切實際，所以我寫了掃描腳本來處理：遇到失格的關閉端，就把句末標點移到強調外面；遇到失格的開啟端，就把成對的括號移到外面；兩者都不適用時，才退而在外側補一個空白。

出錯的是第三次掃描。我把分隔符號**逐行**配對，而只要段落有折行，這是錯的。

```text
…把同一段流程分別跑在釋出版與修正版的打包檔上，**組字中的狀態完全相同，
結果卻不同**：
```

只讀第二行的話，那組 `**` 是該行第一個出現的，所以逐行處理會判定它是開啟端，但它其實是關閉端。掃描於是拿 left-flanking 的條件去檢查它，發現不合格，就在它前面插入一個空白，而那個空白正好會讓關閉端不再是 right-flanking。

*Figure — PairingScope: 同一個關閉端，用兩種方式讀。逐行配對會讓它變成第一組，因而被當成開啟端；以段落為單位配對則讓它維持第二組，也就是它本來的角色。*

**一個修補流程如果可能製造出新的缺陷，就得拿同一套檢查去檢查它自己的輸出**。那三個被我弄壞的段落，是後來把所有檔案重新丟給算繪器跑一遍才發現的，第四次掃描才把它們修了回來。

## 現在擋住它的檢查

規則是兩個字元的函式，所以檢查本身並不複雜。它以段落為單位配對分隔符號，套用兩種 flanking 判定，並把該開啟時開不了、該關閉時關不了的符號都報出來。

```ts title="scripts/prose-check.ts"
const canOpen = !isSpace(next) && (!isPunct(next) || isSpace(prev) || isPunct(prev));
const canClose = !isSpace(prev) && (!isPunct(prev) || isSpace(next) || isPunct(next));
const closing = i % 2 === 1;
if (closing ? canClose : canOpen) continue;
```

有兩個細節比表面上看起來重要。**段落的界線是空行**，不是行號中斷的地方；否則夾在兩個程式碼區塊之間的整段文字會被當成同一個單位。

另一個是，**分隔符號的數量是奇數時要回報，而不是跳過**。單獨一組 `**` 一定會原樣算繪出來，所以它本身就是缺陷。跳過那個範圍，後面的一切就都不再檢查，而且報告上是零缺陷。

檢查要有人親眼看過它失敗，才算得上檢查，所以我故意把一個修好的檔案弄壞。

```bash
node scripts/prose-check.ts <slug> ja
# emphasis delimiters that cannot render: 1
#   line 184  closing  …を削除します。**これはテスト用に起動した…
```

把檔案還原之後，回報數就回到 0，結束碼也是 0。這個檢查只跑 `ja` 和 `zh-tw`，英文產生不出這種形式。

## 總結

1. 一組 `**` 要開啟強調只能靠 left-flanking，要關閉只能靠 right-flanking，而兩種判定都只看前後各一個字元。
2. 前面是標點的關閉端，後面必須是空白或標點，所以 `。**これは` 既開不了也關不了，最後就以星號的原樣出貨。
3. 英文每個句號後面都會寫空白，因此碰不到這個條件。這個缺陷在來源語系裡看不見，一到翻譯版本就會系統性地出現。
4. 修法是把標點移到強調之外；分隔符號緊貼反引號時，外側的空白是退路。
5. 分隔符號要以段落為單位配對。折行的段落可能讓關閉端出現在行首，而逐行處理會把它讀成開啟端。
6. 這條規則只有兩個字元寬，所以它值得交給機器檢查，而不是留給人工校閱。

## 參考連結

- [CommonMark 的強調規則，其中有本文所依據的 left-flanking 與 right-flanking 定義](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis)
- [CommonMark Dingus，會算繪片段並顯示它實際解析出來的 AST](https://spec.commonmark.org/dingus/)
