Markdownの太字（強調）記号`**`がたまに認識されないので緩和用プラグイン作った

> **この`**`は太字として認識されません。**あれ？
>
> よーし**[Qiita](https://qiita.com)**へのリンクを付けて太字・・・あれ？
>
> コードを太字にするぞ・・・よし**`someCode()`**っと。あれ？

Markdownで日本語の文書や記事を書いているとこのような問題に時々ぶち当たります。

## 緩和策

一言で説明すると、Markdown仕様界のデファクトスタンダードである[CommonMark](https://commonmark.org/)の仕様（仕様バグ）です。

二大JavaScript製CommonMark変換ライブラリである[markdown-it](https://github.com/markdown-it/markdown-it)・[remark](https://github.com/remarkjs/remark)に対しては、プラグインを用意したのでお使いください。

- remark向け（MDXなどもこちら）：[remark-cjk-friendly](https://www.npmjs.com/package/remark-cjk-friendly)
- markdown-it向け：[markdown-it-cjk-friendly](https://www.npmjs.com/package/markdown-it-cjk-friendly)

他のMarkdown規格（例：[djot](https://djot.net/)）を採用するという手もありますが、記法が異なる場合がある上、ライブラリの選択肢が限られてしまう傾向にあります。また、ユーザが不特定多数の場合、仕様の違いに戸惑わせてしまうことも考えられます。

## なぜ？

CommonMarkにおける`*`（斜体）・`**`（太字）（以下「強調記号」）の認識にはクセがあります。

強調記号には、解析の段階で「開始記号になりうるか」「終了記号になりうるか」の2種類の属性を与えられます。

それぞれ側の記号に**ならない**条件は、次の**どちらか**を満たす場合です。

1. 内側すぐ隣の文字（以下「内隣」）が空白（全角スペースや文字列の終端も含む。以下同様）の場合
2. 内隣が約物や記号（句読点・カッコなど。以下「約物等」）**かつ**外側の隣（以下「外隣」）が、次のどちらか**以外**
    1. 空白
    2. 約物等

| 開始 / 終了 | 内隣 | 外隣 |
| -- | -- | -- |
| 開始側 | 右隣 | 左隣 |
| 終了側 | 左隣 | 右隣 |

**日本語Markdownで引っかかるのは、基本的に上の2.に合致した場合です。**

1\.で除外されるのは、次のような場合です。

> ```md
> foo* *bar
> ```

2\.の中の1.・2.は、次のようなパターンを受理するためです。

> ```md
> Git **(Not GitHub)** is...
>
> Don't use ... **(Note: the exception is ...)**.
> ```

この仕様は、全部の単語の間をスペースで区切る言語（英語など）を前提としたものです。日本語・中国語のように単語の間はほぼスペースを使わない言語は割りを食います。また、スペースで時々文を区切る言語の中でも、韓国語のように文節単位でしか区切らない言語もたまにこの問題に引っかかります。

https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis

上の原規格書（英語）では、「left-(right-)flanking」に対応します。また、「空白」「約物等」の判定にはUnicodeの文字プロパティを使います。

上の例をそれぞれ見ていきましょう。

> ```md
> **この`**`は太字として認識されません。**あれ？
> ```

1つ目の`**`は左隣が終端（=空白）なので、開始側のみと認識されます。**しかし、2つ目の左隣が「。」（=約物）、右隣が「あ」（空白でも約物等でもない）なので、これも開始側のみとしか認識されません。** そのため、この2つの`**`はペアを作れず、`**`のまま放置されて出力されます。

> ```md
> よーし**[Qiita](https://qiita.com)**へのリンクを付けて太字・・・あれ？
> ```

1つ目の`**`は、右隣（内隣）が「`[`」（カッコ = 約物）、左隣（外隣）が「し」（約物・記号・空白のいずれでもない）なので、開始側として認識されません。2つ目の`**`も、左隣（内隣）が「`)`」（カッコ = 約物）、右隣（外隣）が「へ」（約物・記号・空白のいずれでもない）なので、終了側として認識されません。やはりこの2つとも相方を見つけられずに、`**`のまま放置されて出力されてしまいます。

> ```md
> コードを太字にするぞ・・・よし**`someCode()`**っと。あれ？
> ```

内隣が「`` ` ``」（記号）、外側がひらがな（約物等・空白のどちらでもない）なので意図した側として認識されません。やはり相方を見つけられません。

## 両プラグインが行っている処置

これらのプラグインでは、それぞれのCommonMarkパーサーの一部を乗っ取って以下のような修正を加えています。

- 日本語の約物等はこのルールから除外
- `**`が日本語の隣でもこのルールから除外
- 日本語の判定には[Unicodeで定義されている「東アジアの文字幅」（East Asian Width）](https://ja.wikipedia.org/wiki/%E6%9D%B1%E3%82%A2%E3%82%B8%E3%82%A2%E3%81%AE%E6%96%87%E5%AD%97%E5%B9%85)と絵文字かどうかを利用
- （「𠮷」「𠮟」「𩸽」「<ruby>𰻞<rp>（</rp><rt>ビャン</rt><rp>）</rp></ruby>」などの一部漢字や異体字セレクタ（例：竈門禰󠄀豆子の「禰󠄀」）を正しく認識させる）

これで、日本語Markdownに登場する多くのパターンを網羅できると思います。[詳細な仕様は、英語でまとめています。](https://github.com/tats-u/markdown-cjk-friendly/blob/main/specification.md)


## Qiita・GitHubのMarkdown処理系

Qiitaはこの3つの例を認識しないので、CommonMark準拠と考えられます。

GitHubも同様の例を認識しないので同じです。

Zennも同様のことが報告されているのでおそらくそうでしょう。

## 両プラグインのリポジトリ

上で挙げた両プラグインは、[単一のGitHubのリポジトリ](https://github.com/tats-u/markdown-cjk-friendly)でソースコードを管理しています。

https://github.com/tats-u/markdown-cjk-friendly

**お気に召せば・お役に立てばスター**、バグがあればバグ報告をぜひよろしくお願いします。

## 同じ問題を扱った記事

https://qiita.com/Yarakashi_Kikohshi/items/f5ebd1273cb6c0681da1

https://qiita.com/yusuke_s_yusuke/items/303436e1a164b118936d