VSCode の置換で和欧文間の半角スペースを入れたり取ったりする


# VSCode の置換で和欧文間の半角スペースを入れたり取ったりする

VSCode の置換で和文と欧文の間の半角スペースを入れたり取ったりしたい。

## ■ 動機

フォーマット、組版上の理由と VSCode の特性上の理由の 3 つがある。

フォーマットに関しては言わずもがなと思われるが、組版と VSCode の特性上の理由については詳しく書いておきたい。

### ▽ 組版

一般的に、和欧文の混植を行う場合、和欧文間には “四分アキ” と呼ばれる間隔が設けられる。これは読みやすさを高めるためである。

これは以下のような理由から、手で半角スペースを挿入することが無いようにすべきとされる。

- 半角スペース（二分アキ相当）とは異なる
- これらのアキは組版を処理する際に自動的に挿入されるべき
- 実際にはツメを考慮して四分アキから二分アキまで伸縮する

これは組版処理を行うプログラムに依存する。LaTeX や InDesign などでは和欧文間のアキを調整することが出来る。一方で Markdown や HTML ではこれを処理する機能はサポートされていない。[^akiMdHtml]

[^akiMdHtml]: 実際は CSS を調整することで再現できる。しかし、Markdown で投稿できるサイトでは多くの場合 CSS をいじることが出来ない。

このような事情から、和欧文間の半角スペースについて次のような衝突が生じる。（以降、LaTeX と Markdown を代表として記述する）

- LaTeX では挿入しない
- Markdown では挿入する（諸説あり）

したがって、文章をコピペで LaTeX ⇆ Markdown する際には、欧文間の半角スペースを挿入したり削除したりする必要がある。

### ▽ VSCode の特性

VSCode では特定の文字列からサジェストを表示させることが出来る。しかし、これは区切り文字の右側でないと表示させることが出来ない。

```latex
% 例：
% スニペットに sch と打つと Schrödinger が表示されるように構成していたとする。
%
% サジェストは和文のすぐ右側では表示させることが出来ない
1926 年に sch
      % ↑ sch の左側に半角スペースが無いとサジェストは表示されない
```

そのため、サジェストを和文の右側で表示させたい場合には、半角スペース設ける必要がある。しかし、LaTeX ではこのように挿入された半角スペースもすべて削除したい。

## ■ 方法・方針

:::note info

本記事では、文字種の順序に関して、以下のように表現することとする。

- 左から欧文、和文の順に現れる場合

    ` 欧文->-和文 `

<!--
    ```
    欧文->-和文
    ```
-->

- 順序を問わない場合

    ` 欧文--和文 `

<!--
    ```
    欧文--和文
    ```
-->

また、和文、欧文は次のように構成されているものとする。

- 和文
    - ひらがな
    - カタカナ
    - 漢字
    - 約物
- 欧文
    - ラテン文字
    - アラビア数字
    - 約物

:::

VSCode の正規表現を利用した置換を使って、和欧文間の半角スペースを挿入したり削除したりすることを考える。

和欧文間に半角スペースの挿入/削除を考えるパターンは、以下のような方針に従うことにしたい。

<!-- 各和文の種類と欧文の間に半角スペースを挿入/削除するか。 -->

|            組み合わせ            | Markdown | LaTeX |
| :------------------------------: | :------: | :---: |
|            和文--和文            |   削除   | 削除  |
|    欧文--ひらがなカタカナ漢字    |   挿入   | 削除  |
|          欧文--和文約物          |   削除   | 削除  |
|   和文->-開き括弧（欧文約物）    |   挿入   | 削除  |
|   閉じ括弧（欧文約物）->-和文    |   挿入   | 削除  |
| 行内コード--ひらがなカタカナ漢字 |   挿入   |   -   |
|       行内コード--和文約物       |   削除   |   -   |

また、以下を要請する。

- “欧文--欧文” 間には過剰な半角スペースを挟まない
- 全角英数は使わない（使用する場合、欧文として処理する）
- 和文が含まれる丸括弧は `（` `）` を利用する（`(` `)` を利用しない）

これ以降、本記事では和文や欧文への正規表現を用いたマッチの方法が多くを占めることになります。置換方法のみ知りたい場合には、[\# 実用例](#-実用例) まで飛んでください。

### ！VSCode における正規表現による置換の注意点

VSCode の正規表現は ECMAScript をベースとしている。今回 Unicode property を利用するため、以下を参照して利用可能なカテゴリを確認する必要がある。

- https://262.ecma-international.org/10.0/#table-nonbinary-unicode-properties
- https://262.ecma-international.org/10.0/#table-binary-unicode-properties

VSCode では Unicode property に `gc`、`sc`、`scx` の 3 つを採ることが出来る。

- `General_Category`（`gc`/省略可）
- `Script` (`sc`)
- `Script_Extensions` (`scx`)

ただし、`Block` (`blk`) は利用することが出来ない。

## ■ 和文にマッチする

Unicode property は次の 3 つによって指定する。

- ひらがな：`sc=Hiragana`
- カタカナ：`sc=Katakana`
- 漢字：`sc=Han`

また、いくつかの約物についてもマッチさせたい。

### ▽ ひらがなカタカナ漢字

ひらがなカタカナ漢字は Unicode property を用いて `\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Han}` によって指定する。

ここで、Hiragana、Katakana、Han の代表的なものを一覧にしておく。

- `sc=Hiragana` (380 code points) [<sup>⧉</sup>][hiragana]
    - ひらがな（濁点、半濁点を含む）
    - 変体仮名
    - ` 🈀 `
- `sc=Katakana` (320 code points) [<sup>⧉</sup>][katakana]
    - カタカナ（濁点、半濁点を含む）
    - 半角カタカナ
    - 丸囲みカタカナ
    - カタカナ組文字
- `sc=Han` (94215 code points) [<sup>⧉</sup>][han]
    - 漢字がいっぱい
    - 中国語で利用される漢字も含まれる

[hiragana]: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7Bsc%3DHiragana%7D&g=&i=
[katakana]: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7Bsc%3DKatakana%7D&g=&i=
[han]: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7Bsc%3DHan%7D&g=&i=

ただし、`sc` には ` ー `（長音）や `・`（中黒）などが含まれていない。そのため、これらを含む `scx` によって指定する。また、`scx=Hiragana/Katakana/Han` には `、` `。` `「` `」` 等の約物も含まれている。

#### 波ダッシュ記号

日本語の波ダッシュ記号は ` 〜 ` が利用される。

しかし、何も考えずに波ダッシュをキーボードで入力する ` ～ ` は `\uFF5E` (Fullwidth tilde) となる。これは “本物の” 波ダッシュではない。（見た目にほとんど差異が無い）

そのため、これは置換しておきたい。

|              Unicode name              | 符号位置 | サンプル |
| :------------------------------------: | :------: | :------: |
|            Fullwidth Tilde             | `\uFF5E` |  ` ～ `  |
|               Wave Dash                | `\u301C` |  ` 〜 `  |
| Katakana-Hiragana Prolonged Sound Mark | `\u30FC` |  ` ー `  |

Wave dash を波ダッシュを長音符として利用する場合を考慮して、約物を含まない和文をマッチする際にこれを利用する。

```regexp
[\u30FC\u301C]
```

### ▽ 約物

`scx=Hiragana/Katakana/Han` では多くの約物が含まれているものの、いくつかの約物・記号は含まれていない。ここでは、それらもマッチ出来るようにしたい。

和文の文章では次のような約物・記号を使うこともあるだろう。

```
，；：！？．＇＂（）［］｛｝｟｠＠＊／＼＆＃％＋＜＝＞＄￥
```

これは次のように Unicode で指定しておく。[<sup>⧉</sup>][block_halfwidth_and_fullwidth_forms]

[block_halfwidth_and_fullwidth_forms]: https://www.compart.com/en/unicode/block/U+FF00

```regexp
[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5]
```

### ▼ 結論

- 約物を含む場合

    ```regexp
    ([\p{scx=Hiragana}\p{scx=Katakana}\p{scx=Han}]|[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5])
    ```

- 約物を含まない場合

    ```regexp
    ([\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Han}]|[\u30FC\u301C])
    ```

## ■ 欧文をマッチする

Unicode property は次の 2 つによって指定する。

- ラテン文字：`sc=Latin`
- 数字：`N`

この他にもさまざまな言語が含まれると思われるが、ここではこの 2 つのみとしておきたい。

また、いくつかの約物についてもマッチさせたい。

### ▽ ラテン文字

ラテン文字は `\p{sc=Latin}` によってマッチさせることが出来る。特にこれ以上に考えることは無いと思われる。`sc=Latin` は `scx=Latin` にしても約物は含まれないことに注意が必要になる。

この他の言語が必要な場合には、逐次追加すると良いだろう。例えば、キリル文字は `\p{sc=Cyrillic}` である。

### ▽ 数字

実は、数字に関するマッチはかなり面倒である。`\d` や `\p{N}` ではさまざまな言語における数字が含まれている。

> [Unicode にある数字の一覧 | hydrocul のメモ](https://hydrocul.github.io/wiki/blog/2015/1129-unicode-general-category-number.html#nl)

しかしながら、ここに漢数字は含まれていないため、問題ないものとして `\p{N}` を利用する。

### ▽ 約物

欧文として次の約物をマッチさせたい。

```
,;:!?.'()`<>‘’“”
```

Unicode で指定すると少しややこしいので、これらは直接マッチさせることとする。

```regexp
[,;:!?.'()`<>‘’“”]
```

この他、フランス語の引用符 (`«` `»`) など必要な約物があれば、ここに追加する。

ただし、“和文->-欧文” の場合と “欧文->-和文” の場合があるため、次のように 2 つに分ける。“和文->-欧文” は開き括弧のみ、“欧文->-和文” は `,;:!?.` と閉じ括弧を指定している。

```regexp
([\p{sc=Latin}\p{N}]|[(`<‘“])
([\p{sc=Latin}\p{N}]|[,;:!?.')>’”])
```

ここには以下の記号はあえて含めていない。これらは Markdown や LaTeX でマークアップする上で重要な記号だからである。（指定すると予期せぬ置換が起こる可能性がある）

```
- [ ] { } @ * / \ & # % + | $
```

### ▼ 結論

- 条件のない場合

    ```regex
    ([\p{sc=Latin}\p{N}]|[,;:!?.'()`<>‘’“”])
    ```

- 和文->-欧文に対応する場合

    ```regex
    ([\p{sc=Latin}\p{N}]|[`(<‘“])
    ```

- 欧文->-和文に対応する場合

    ```regex
    ([\p{sc=Latin}\p{N}]|[,;:!?.')>’”])
    ```

## ■ 実用例

Replace Rules を利用した置換を考える。

> [bhughes339.replacerules](https://marketplace.visualstudio.com/items?itemName=bhughes339.replacerules)

以下の 13 個を設定する例を示してみたい。これらは言語によって制限が加えられている。

- 半角スペースの削除
    - 欧文--和文
    - 和文--和文
    - 欧文--欧文の間の過剰な半角スペース
    - LaTeX における引用符--引用符の外側の和文
- 半角スペースの挿入
    - 欧文--和文
    - マークアップされた欧文--和文
    - 行内コード--和文
- Fullwidth tilde から Wave dash に置換

<details>
<summary><strong> 設定例（折りたたみ）</strong></summary>
<div>

`` ` `` による行内コードのみ特別な処理をしていますが、期待通りではない可能性があります。

13 つの Rule は Rule set によってまとめることが出来るため、これを `Format: Markdown, HTML, LaTeX` とした。

もしも、半角スペースを挿入したくなく、半角スペースを削除したいのみの場合には、`Insert space` から始まる Rule を Rule set から外せば良いだろう。

```jsonc
// settings.json
{
  "replacerules.rules": {
    "Remove space: nonJpn-Jpn": {
      "find": "([\\p{sc=Latin}\\p{N}]|[,;:!?.')>’”])( )+([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}]|[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5])",
      "replace": "$1$3",
      "flags":"gu",
      "languages": ["markdown", "html", "latex"]
    },
    "Remove space: Jpn-nonJpn": {
      "find": "([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}]|[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5])( )+([\\p{sc=Latin}\\p{N}]|[(`<‘“])",
      "replace": "$1$3",
      "flags":"gu",
      "languages": ["markdown", "html", "latex"]
    },
    "Remove space: Jpn-Jpn": {
      "find": "([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}]|[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5])( )+([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}]|[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3D\uFF5B\uFF5D\uFF5F\uFF60\uFFE5])",
      "replace": "$1$3",
      "flags":"gu",
      "languages": ["markdown", "html", "latex"]
    },
    "Remove many spaces: nonJpn-nonJpn": {
      "find": "([\\p{sc=Latin}\\p{N}]|[,;:!?.'()`<>‘’“”])( ){2,}([\\p{sc=Latin}\\p{N}]|[,;:!?.'()`<>‘’“”])",
      "replace": "$1 $3",
      "flags":"gu",
      "languages": ["markdown", "html", "latex"]
    },
    "Remove space: before LaTeX-quote-symbol": {
      "find": "([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}])( )+(`+)",
      "replace": "$1$2",
      "flags":"gu",
      "languages": ["latex"]
    },
    "Remove space: after LaTeX-quote-symbol": {
      "find": "('+)( )+([\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Han}]+)",
      "replace": "$1$2",
      "flags":"gu",
      "languages": ["latex"]
    },
    "Insert space: nonJpn-Jpn": {
      "find": "([\\p{sc=Latin}\\p{N}]|[,;:!?.')>’”])( )*([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])",
      "replace": "$1 $3",
      "flags":"gu",
      "languages": ["markdown", "html"]
    },
    "Insert space: Jpn-nonJpn": {
      "find": "([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])( )*([\\p{sc=Latin}\\p{N}]|[(`<‘“])",
      "replace": "$1 $3",
      "flags":"gu",
      "languages": ["markdown", "html"]
    },
    "Insert space: after inline-code": {
      "find": "(`+)( )*([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])",
      "replace": "$1 $3",
      "flags":"gu",
      "languages": ["markdown"]
    },
    "Insert space: before inline-code": {
      "find": "([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])( )*(`+)",
      "replace": "$1 $3",
      "flags":"gu",
      "languages": ["markdown"]
    },
    "Insert space: after markup": {
      "find": "(\\*{1,3}|~~)([[\\p{sc=Latin}\\p{N}]+|[\u0021-\u007E]+)(\\1)( )*([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])",
      "replace": "$1$2$3 $5",
      "flags": "gu",
      "languages": ["markdown"]
    },
    "Insert space: before markup": {
      "find": "([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])( )*(\\*{1,3}|~~)([[\\p{sc=Latin}\\p{N} ]+|[\u0021-\u007E]+)(\\3)",
      "replace": "$1 $3$4$5",
      "flags": "gu",
      "languages": ["markdown"]
    },
    "Replace: Fullwidth-tilde to Wave-dash": {
      "find": "\uFF5E", // Fullwidth tilde
      "replace": "\u301C", // Wave dash
      "flags": "gu",
      "languages": ["markdown", "html", "latex"]
    }
  },
  "replacerules.rulesets": {
    "Format: Markdown, HTML, LaTeX": {
      "rules": [
        "Replace: Fullwidth-tilde to Wave-dash",
        "Remove space: nonJpn-Jpn",
        "Remove space: Jpn-nonJpn",
        "Remove space: Jpn-Jpn",
        "Remove many spaces: nonJpn-nonJpn",
        "Remove space: before LaTeX-quote-symbol",
        "Remove space: after LaTeX-quote-symbol",
        "Insert space: nonJpn-Jpn",
        "Insert space: Jpn-nonJpn",
        "Insert space: before inline-code",
        "Insert space: after inline-code",
        "Insert space: before markup",
        "Insert space: after markup"
      ]
    }
  }
}
```

</div>
</details>

今回、文末の `？` や `！` の後ろに全角スペースを挿入するような置換は作成していない。これを実現したい場合には以下のようにすれば良いだろう。ただし、このような置換は、文中の `？` や `！` の後ろにも全角スペースが挿入されてしまうため、注意が必要になる。

```json
  "Insert Zenkaku space: after emotion symbol": {
    "find": "([？！]+)(　)*([\\p{sc=Hiragana}\\p{sc=Katakana}\\p{sc=Han}]|[\u30FC\u301C])",
    "replace": "$1　$2",
    "flags": "gu",
    "languages": ["markdown", "html"]
  }
```

`？` や `！` のアキ量に関しては以下の W3C の JLReq を参照してほしい。

> [§3.1.6 区切り約物及びハイフン類の配置方法 \- Requirements for Japanese Text Layout 日本語組版処理の要件（日本語版）](https://www.w3.org/TR/jlreq/#positioning_of_dividing_punctuation_marks)

ちなみに、これを利用して句読点を (`、` `。`) から (`，` `．`) に変更するための Rule を作成することも出来る。

### ▽ 課題

行内コードに和文が含まれる場合、少しだけ期待しない問題が生じる。`` ` 日本語 ` `` となる。

以下にテストコードを載せておく。

- `Insert space: before inline-code`

    https://regex101.com/r/2qw27q/1

- `Insert space: after inline-code`

    https://regex101.com/r/QjfcF4/1

行内コードに和文が含まれることはほとんどないと思われるため問題ないとだろう。実際は、行内コードの周囲の和文のみにマッチするようにしたかったが、正規表現が難しいため諦めた。良いマッチを作成できた人がいれば教えてほしい。

# 余談

LaTeX ⇆ Markdown のような置換やフォーマットなど少しはやりやすくなるのではないだろうか。
まだ抜け目がある可能性があるため、修正が必要な場合は適宜調整してほしい。

正規表現むずかしい。言語によって仕様が違う。どうにかしてほしい。

特に調べずに Markdown ファイルの和欧文間に半角スペースを挿入することを考えていたが、どうやら Prettier を利用する人から和欧文間の半角スペースの挿入は問題視されていたようだ。時代に逆行する記事だったかもしれない。

> [和欧文字間（漢字仮名と英数字の間）に半角スペースが挿入されないようにするPrettier Markdownプラグインを作った - Qiita](https://qiita.com/tats-u/items/bcbfe2bb4e71bf0a2b87)

まぁ、Prettier を利用せずにフォーマット出来るので、別の方策として利用できると思えば良いだろう。

個人的には、アキ量が異なるとは言え和欧文間にアキが無いと読みづらく感じるので、Markdown では和欧文間に半角スペースを挿入したい。CSS から対応されれば、半角スペースを取り除くけれど。

実際、Qiita だと和欧文間に半角スペースを挿入すると四分アキくらいになるのは気のせいかしら。そういうフォントなのかも。

# 追記

- 2022/07/16: Fullwidth tilde と Wave dash との違いに対応。LaTeX における引用符に対応。軽微修正。
- 2022/08/08: マークアップした欧文と和文との間への挿入に対応。軽微修正。

<!--
- 一部の波ダッシュを全角チルダに書き変えてください
- プラグインを作った記事のタイトルの和欧文間に半角スペースを削除してください
-->