Typstで和文と数式の間の空きをどうにかしたい話

本記事では、Typstにおける有名な未解決問題である**​「和文と数式の間の空き」​**について、問題の回避策について検討していきます。

## やりたいやつ

伝統的な和文組版においては「和文と欧文の間に小さい水平空きを入れる」という規則になっています。以下ではこの空きのことを**​「和欧文間空白」​**と呼ぶことにします。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/bdb71b95-370c-0f57-cb12-d3a2a8291fba.png)

伝統的な“全角組”においては和欧文間空白の標準的な幅は「全角の1/4」（四分）となっています[^shibuaki]。一般的な欧文フォントでは欧文空白の幅は全角の1/3～1/4程度であることが多いため、フォントによっては和欧文間空白は欧文空白よりも小さくなります。

[^shibuaki]: そのため、印刷業界では和欧文間空白のことを指して**​「四分空き」​**と呼びます。

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/92ceec93-520d-5c4d-915d-1491b4c2662a.png)

:::note info
“プロポーショナル組”の場合の和欧文間空白は四分よりも小さい値が好まれ「空きを入れない」という選択もあるようです。
:::

和文文書においては数式も“欧文の一種”であるため、**和文と数式の間**にも和欧文間空白を入れる必要があります。

![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/7dd8e1ab-4fcb-e88d-ec20-aa16b7e4e5a3.png)

和文側の文字種によっては和欧文間空白が入らない場合もあります。例えば、括弧の内側や句読点の直前には空きを入れてはいけないため、この位置に欧文や数式があっても和欧文間空白は入りません[^outside]。

[^outside]: 括弧の外側については少し事情が複雑なのですが、組版的には“既に空きが入っている”ことになっているため、ここにも和欧文間空白は入りません。

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/0d783943-db0b-4156-96a6-c99c6d30eb4c.png)

以上が伝統的な和文組版における和欧文間空白の規則です。

:::note info
以降では伝統的な“全角組”を行うことを前提にします。
:::

## おやくそくのやつ

以降ではTypstにおける和欧文間空白の扱いについて述べますが、そこでの出力例にでは和文用の最低限のレイアウト設定として次のモジュールを使用します。

```text:layout.typ
// 和文用の最低限のレイアウト設定
#let doc(body) = {
  set page(paper: "a5")
  set text(
    lang: "ja",
    font: ("New Computer Modern", "Harano Aji Mincho"),
    weight: 450,
    top-edge: 0.88em,
  )
  body
}
```

この設定では和文書体に[「原ノ味明朝」][haranoaji]、欧文書体に[「New Computer Modern」][newcm]のBookウェイト[^newcm]を指定しています。この欧文フォントの欧文空白の幅は0.333em（ほぼ三分）なので和欧文間空白との違いが顕著になります。（実はこのフォント設定を前節の例でも使っています。）

[^newcm]: Typstの既定の数式フォントが「New Computer Moder Math」のBookウェイトであり、これと同じ書体・ウェイトのテキストフォントを選んでいます。ちなみに、この設定は奥村晴彦氏による記事[「Typst入門」][okumura]で紹介されているものです。

:::note info
従って、以下の例を実際にコンパイルするには「原ノ味明朝」と「New Computer Moder Math」のBookウェイト[^newcm-book]がTypstで使用可能になっている必要があります。
:::

[^newcm-book]: テキスト用（Mathでない）の「New Computer Modern」のRegularとBoldのウェイトはTypstに内蔵されていますが、Bookはされていないので別途入手する必要があります。ファイル名でいうと`NewCM10-Book.otf`と`NewCM10-BookItalic.otf`です。

## 現状のTypstで和欧文間空白を自動挿入するやつ

Typstは日本語組版もサポート範囲に含めているため、和欧文間空白の**自動挿入**の機能をもっています。和欧文間空白の自動挿入は`text`エレメントの`cjk-latin-spacing`パラメタで制御できます。

  * `cjk-latin-spacing`［none｜auto；既定値＝`auto`］： 和欧文間空白の自動挿入の状態。
      * `none`： 和欧文間空白を挿入しない。
      * `auto`： “全角組”の規則に従って、文書ソース上で和文と欧文が隣接している箇所について必要に応じて四分（0.25em）の空きを自動挿入する[^propo]。

[^propo]: 現状では`cjk-latin-spacing`の値は`none`（空き無し）か`auto`（四分）の2通りしかないため、“プロポーショナル組”で「四分より小さい和欧文間空白を自動挿入する」という要求はサポートされていません。一方で「和欧文間空白を空けない」という選択は`none`を指定することで実現できて、しかもこの記事のメインの話題である「数式で和欧文間空白が自動挿入されない」という性質はそもそも問題にならなくなります。

既定値が`auto`なので、ソース上で和文と欧文が隣接している部分には和欧文間空白が適切に自動挿入されます。もし何らかの理由で自動挿入を止めたいという場合は、set規則等を利用して`text`エレメントの`cjk-latin-spacing`に`none`を指定すればよいわけです。

```text:example-1.typ
// おやくそくのやつ
#import "layout.typ": *
#show: doc

// 既定で和欧文間空白の自動挿入は有効
お客様の中にTypst芸人はおられませんか。

// 自動挿入を無効にする
#set text(cjk-latin-spacing: none)
お客様の中にTypst芸人はおられませんか。
```

![image-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/5bc1c51a-56b2-20e1-ab87-27d851a56231.png)

## 現状のTypstで和欧文間空白を自動挿入できないやつ

ところが現状のTypstでは、和欧文間空白の自動挿入機能にはまだまだ**不完全**なところがあります。和文と欧文の間には空白が入るのですが、**和文と数式**の間には空白が**入らない**のです。

```text:example-2.typ
#import "layout.typ": *
#show: doc

// 数式との間いには空白が入らない!
お客様の中に$T_y (p_s dot tau)$芸人はおられませんか。
```

![image-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/afd6acec-e437-54d3-8eee-bf960f14f008.png)

これは結構厄介な問題です。「自動で入らないなら**手動**で入れよう」と思っても、正しい**四分**の空きを入れる“イイカンジの”方法がなかなか見つからないからです。もちろん、単純にASCIIの空白文字を入れてしまうと**欧文空白**（今の設定では三分）が入ってしまうので正しい結果になりません。

:::note info
次に示す例では差異を明確にするため、数式の先頭の“T”を欧文の例に合わせて立体`upright(T)`にしています。
:::

```text:example-3.typ
#import "layout.typ": *
#show: doc

お客様の中にTypst芸人は…

お客様の中に $upright(T)_y (p_s dot tau)$ 芸人は…
```

![image-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/62c94c43-9ec8-77ed-ffd5-7d71c5f05775.png)

もっとも「こんな僅かな差はほとんどの人は気にしない:blush:」のも確かでしょうし、この問題が将来のバージョンで解決される見込みも高い[^issue]ので、今のところはこれで妥協しておく:upside_down:という方策も考えられるでしょう。

[^issue]: Typstの開発側で[この問題][issue]は既に認識されています。

しかし一方で日本語LaTeXでは和文と数式の間の空白自動挿入はしっかりサポートされています。よって「（日本語文書について）**TypstがLaTeXの代わりになる**:muscle:」と主張したい人にとっては、現状でも何らかの回避策が必要なのは間違いありません。

## 現状のTypstの問題を回避するやつ

というわけで、以降では「和文と数式の間に和欧文間空白が入らない」問題に対する**回避策**を検討していきます。自動で入らないのを解決するのは困難そうなので、まずは**手動**で入れることとし、以下の要件を定めます。

* 正しい幅である四分の空きが入ること。
* ソース上の記述が煩雑になることをできるだけ避けること。

### 四分空きのモノを作るやつ

正しい幅の空きを入れること自体は難しくありません。水平空きを生成する`h()`関数を利用して、マークアップモードで`#h(0.25em)`と書けば所望の四分空きが出力されます。

```text:example-4.typ
#import "layout.typ": *
#show: doc

// "#h(0.25em)"で四分の空きが入る
お客様の中に#h(0.25em)$T_y (p_s dot tau)$#h(0.25em)芸人は…
```

![image-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/96b16d1f-dab6-59cf-384b-80bf6c327e1b.png)

求める出力は得られましたが、明らかにこの記述は**煩雑**すぎます。

`h(0.25em)`の結果を変数に保存しておくと毎回この式を書く必要がなくなります。できるだけ簡潔にしたいので、変数名も**1文字**で`q`にしておきましょう。

```text:example-5.typ
#import "layout.typ": *
#show: doc

// 四分の空きのコンテンツ
#let q = h(0.25em)

お客様の中に#q$T_y (p_s dot tau)$#q;芸人は…
```

※出力はexample-4.typと同じです。

:::note info
数式の後ろの`#q;`の`;`は、`#`で始まるスクリプトモードの式を強制的に終結させるためのものです。もし`;`を除いて`#q芸人は…`と書くと`q芸人は`という未定義の変数を参照しようとしてエラーになります。
:::

かなりマシにはなりましたが、それでも「数式の前後のほぼ全てに`#q`が挟まっている」のはどうにも煩雑な感じがします。

```text
ここで#q$a$#q;と#q$b$#q;を#q$n$#q;以下の自然数とする。
```

これはやはり実用には耐えられないですね:tired_face:

:::note info
LaTeXでは`\>`のような「記号からなる命令名」の使用により煩雑さを抑えられます[^bxjscls]。また分割禁止欧文空白を表す`~`も（TeXレベルでは）再定義可能なので、CJKパッケージ[^latex-cjk]では「`~`を和欧文間空白のために再割当する」という“荒業”を用いています。残念ながらTypstではこういう芸当が使えないので「超簡潔に書く」というのが難しいのです。
:::

[^bxjscls]: 実際にBXjsclsバンドルの文書クラスでは`\>`を「手動で和欧文間空白を入れる命令」と定義しています。
[^latex-cjk]: pdfLaTeXでCJK言語を扱うためのパッケージです。和欧文間空白については、自動挿入する機能はないので`~`で手動で挿入するという規則になっています。

### 四分空きの文字を書くやつ

先述の通り、Typstでは「1文字の記号にユーザ定義を割り当てる」ことはできないわけですが、でもよく考えてみると、Unicode文字の中には最初から**​「四分の空き」という意味の文字**が存在します。それがU+2005“FOUR-PER-EM SPACE”という文字です。

Typstでは任意のUnicode文字が使用可能で、しかもこのU+2005は“普通の文字”なのでそのまま出力されます。つまり**U+2005の1文字**で求める「四分の空き」が出せるわけです:slight_smile:

```text:example-6.typ
#import "layout.typ": *
#show: doc

// 数式の前後にはU+2005が入っている
お客様の中に $T_y (p_s dot tau)$ 芸人は…
```

※出力はexample-4.typと同じです。

この方法の難点は「U+2005の**文字の取扱**が煩わしい」ということでしょう。実用的に利用するためには、使用するエディタにおいてU+2005が“イイカンジ”に入力できてかつ“イイカンジ”に表示されている必要がありそうです。でも条件が揃えば有望な方策になるかもしれません。

### 欧文空白が四分なフォントを使うやつ【オススメ①】

手動で空白を入れるという前提の下では「ASCIIの空白文字を入れる」というのが最も簡潔な記述であることは間違いありません。これが使えない原因は「欧文空白が四分ではないから」でした。

でも改めて考えると「欧文空白の幅」はフォントによって違います。だったら「**欧文空白が四分**であるフォント」を指定しておけばいいのでは？:thinking:

でも、そんなうまい欧文フォントがあるでしょうか……、実はあります。なんと、Typstの**既定のフォント[^libertinus]である「Libertinus Serif」がまさに条件を満たしています**:astonished:

[^libertinus]: Typstの既定の（初期状態で指定されている）フォントは0.12.0版において「Linux Libertine」から「Libertinus Serif」に変更されました。ただし後者は前者の派生フォントであるため書体としては同じです。そして欧文空白の幅についても両者ともに四分（0.25em）です。

「Libertinus Serif」を欧文フォントとして指定しておけば「ASCIIの空白文字を入れる」という簡潔な記述が通用するようになります。

:::note info
フォントが変わるため、以下の例では、“おやくそく”のレイアウトlayoutを使わずに独自のレイアウト設定を先頭に書いてあります。
:::

```text:example-7.typ
// "おやくそく"以外のレイアウト設定
#set page(paper: "a5")
#set text(
  lang: "ja",
  // 欧文フォントを"Libertinus Serif"にした
  font: ("Libertinus Serif", "Harano Aji Mincho"),
  top-edge: 0.88em,
)

// 再び欧文と数式を比較する

お客様の中にTypst芸人は…

// 数式は"New Computer Modern Math"なので書体は異なる
お客様の中に $upright(T)_y (p_s dot tau)$ 芸人は…
```

![image-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/7fd54239-fb91-a5ae-6cab-383b683181d1.png)

既定フォントである「Libertinus Serif」はTypstに内蔵されているため、Typstにおいて貴重な「必ず使えるフォント」の1つになっています。このため日本語文書でもこのフォントを使っている人は多いでしょう。実はその“よくある設定”の場合は今まで散々議論してきた「和欧文間空白の幅」の問題はそもそも生じていないのでした:upside_down::upside_down::upside_down:

### 欧文空白を無理やり四分にするやつ【オススメ②】

「欧文空白が四分であれば嬉しい」という考え方は「Libertinus Serif」（などの欧文空白が四分であるフォント）以外を使う場合にも応用できます。実は、Typstには「**欧文空白の幅を変更**する」機能が用意されています。`text`エレメントの`spacing`パラメタです。

* `spacing`：［relative[^relative]；既定値＝`100%+0pt`］ 欧文空白の幅を表す。値は「現在フォントの属性で定められる標準の欧文空白幅」を基としたrelative値で指定する。

[^relative]: relative型はratio値とlength値の和として表される値を表します。例えば`50%+10pt`や`0%+2em`等で、後者の値は`2em`とも書けます。つまり、ratio値やlength値はそのままrelative値として使用可能です。

つまり`spacing`パラメタに`0.25em`（＝`0%+0.25em`）という値を指定すると、フォントの欧文空白の幅（これが`100%`）が何であっても欧文空白の幅を0.25em（四分）に変更できます。

「New Computer Modern」フォントを用いた“おやくそく”の設定にこの`spacing`指定を適用してみましょう。

```text:example-8.typ
// "おやくそく"の設定
#import "layout.typ": *
#show: doc
// さらにコレ
#set text(spacing: 0.25em)

// 再び欧文と数式を比較する

お客様の中にTypst芸人は…

お客様の中に $upright(T)_y (p_s dot tau)$ 芸人は…
```

![image-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/b56bae1c-5b8b-13a2-8878-62eb0bebaea7.png)

欧文フォントが何であっても、「ASCIIの空白文字を入れる」という簡潔な記述が使えるようになりました:smiley:

## それでも自動で和欧文間空白を入れたいやつ

前節では空白を**手動**で入れることを前提にして、正しい結果を得ることに注力してきました。しかし、最も理想的な解決はやはり「数式の前後に何も（空白文字も）書かなくても和欧文間空白が**自動挿入**される」ことでしょう。本節ではこれの実現可能性について検討してみます。

### あかんやつ

和欧文間空白を自動で挿入する方法としてネット上で時折見かけるのが「数式（`math.equation`エレメント）に対して、前後に和欧文間空白を入れるようなshow規則を設定する」というものです。

```text:example-9.typ
#import "layout.typ": *
#show: doc

// あかんやつ
#show math.equation.where(block: false): it => {
  h(0.25em); it; h(0.25em)
}

お客様の中に$T_y (p_s dot tau)$芸人はおられませんか。
```

![image-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/23c61463-ec52-d69e-330d-0526d53a48a3.png)

この例では正しい結果が得られているように見えます。

しかしこの方法は結局**うまくいきません**:no_good: 何故かというと、この文書の最初で述べたように、数式の前後に**必ず和欧文間空白が入るとは限らない**からです。

```text:example-10.typ
#import "layout.typ": *
#show: doc

// あかんやつ
#show math.equation.where(block: false): it => {
  h(0.25em); it; h(0.25em)
}

// 和欧文間空白が入らないはずの箇所に入ってしまう!
アレ（$T_y (p_s dot tau)$）、つまり$T_y (p_s dot tau)$。
```

![image-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/d112ade8-505b-34a4-efb7-e79ab5abfac7.png)

この例の出力結果は明らかに妥当ではありません。

<details><summary>補足</summary>

この問題に対する解決策として「現実の運用では和欧文間空白が入る場合が圧倒的で入らない場合は極めて少ないはずだから、自動で空きを入れるshow規則を適用した上で、**入らない場合にその空きの効果を打ち消す**ための何らかの記述を書く規則にする」というアイデアが考えられます。しかしTypstで「空きの効果を打ち消す」のは困難だと思われます。

空きを消すこと自体は`h(-0.25em)`でできます。しかし数式の前後が元々行分割禁止の箇所になっていた場合、空きを入れることでその行分割禁止が破れてしまうことになり、これを防ぐ手立てがありません。（そもそも和欧文間空白が入らないパターンは「括弧の内側」など行分割禁止が同時に適用されていることが多いことに注意してください。）

結局、空きをが入らない箇所では「自動で入る空きを最初から抑止する」必要があるのですが、Typstでそういう処理を実装するのは、introspectionを駆使したかなり複雑なコードを実装する必要があると考えています。

</details>

### やっぱりあかんやつ

どうやら和欧文間空白の自動挿入の処理をTypstのコードで“実装”するのはかなり無謀そうです。「Typstの既存の処理をうまく活用する」方策を考えると、「数式の部分を無理やり**欧文であると認識させる**」という方針が思いつきます。これは日本語LaTeXの世界では有名なトピックであり、解決策として「**ゴースト**挿入」という技法がよく知られています。

* [BXghost: (u)pTeX 以外でも適切に和欧文間スペースを入れたい][wtsnjp]（ラング・ラグー）

「**ゴースト*」というのは「何らかの望ましい性質をもつ**幅ゼロの見えない**文字」を指します。例えば「欧文ゴースト」は「和欧文混植処理において欧文文字のように振る舞うゼロ幅不可視文字」のことです。日本語LaTeXの世界ではU+200B“ZERO WIDTH SPACE”（に相当する文字）が欧文ゴーストとしてよく用いられます。

欧文ゴースト挿入をTypstでやってみましょう。数式に対するshow規則において、先ほどは「四分空きを両端に付ける」処理をしていましたが、今度は代わりに「U+200Bの文字を両端に付ける」処理を行います。

```text:example-11.typ
#import "layout.typ": *
#show: doc

// いけるかもしれないやつ
#show math.equation.where(block: false): it => {
  let ghost = "\u{200B}" // 欧文ゴースト
  ghost; it; ghost
}

お客様の中に$T_y (p_s dot tau)$芸人はおられませんか。
```

早速コンパイルしてみましょう。

![image-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/63fa7b70-ceb3-d7a6-945c-4de2777c853b.png)

**うまくいきません**:sob:

これが失敗する理由は「U+200Bは**​“欧文”の文字ではない**」（制御文字の一種）だからです。TypstはUnicodeのマトモな知識を持っていて、現状の仕様では「ラテン文字・ギリシャ文字・キリル文字・算用数字の何れか[^al-letter]に該当する文字」を欧文文字と見なしているようです。

[^al-letter]: 実際には若干の記号を追加しています。

Unicodeのゼロ幅不可視文字はU+200Bの他にもありますが、そういう文字は当然特定の用字系には属しません。残念ながら、Typstでは欧文ゴーストとして使える文字は存在しません。

### 超絶アレなやつ

絶望的な状況:sob:になってしまいましたが、もう少し考えてみましょう。Typstは組版ソフトウェアなので欧文ゴーストの文字を出力する際に好きな**フォントを選択**できます。ということは「Typstが欧文文字と見なす文字」がゼロ幅不可視になっている“特別なフォント”を用意すればいいのでは？

……ということは**コレ**の出番ですね:wink:

* [Adobe Blank][adobe-blank]（GitHub/adobe-fonts）

「Adobe Blank」はAdobeがOSSとして提供しているフォントで**​「全ての文字がゼロ幅不可視」​**というトッテモ素敵:upside_down:なデザインを持っています。これを使えば目的の「欧文ゴースト」が実装できそうです。

先ほどの欧文ゴースト挿入の実装で「U+200Bの文字」の代わりに「Adobe BlankのU+0375の文字」に変えてみます。

:::note info
U+0375“͵”(GREEK LOWER NUMERAL MARK)はギリシャ語で使われる記号の一種です。欧文ゴーストの文字はPDFからコピペした場合に混入してしまうため、できるだけ目立たない記号である（かつ数式中で使われることがない）文字を選びました。
:::

```text:example-12.typ
#import "layout.typ": *
#show: doc

// 超絶アレなやつ
#show math.equation.where(block: false): it => {
  let ghost = text(font: "Adobe Blank", "\u{375}") // 欧文ゴースト
  ghost; it; ghost
}

お客様の中に$T_y (p_s dot tau)$芸人はおられませんか。

// 四分空きが入らないパターンも試してみる
アレ（$T_y (p_s dot tau)$）、つまり$T_y (p_s dot tau)$。
```

:::note info
この例をコンパイルするには「Adobe Blank」のフォントがTypstで使用可能になっている必要があります。（自分はTTF版の`AdobeBlank.ttf`を使いました。）
:::

![image-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/0ac6e146-5afa-fea8-2863-663445014a82.png)

**うまくいきました！**:heart_eyes: やっぱり「Adobe Blank」の力は絶大（えっ:astonished:）

## まとめ

和文と数式の間の和欧文間空白を実現する、現状でオススメの方法は次の2つです。

  * [方法①](#欧文空白が四分なフォントを使うやつオススメ)：欧文空白の幅が四分である欧文フォント（Typstの既定の「Libertinus Serif」でよい）を使った上でASCII空白文字を手動で入れる。
  * [方法②](#欧文空白を無理やり四分にするやつオススメ)：`#set text(spacing: 0.25em)`で欧文空白の幅を四分に変更した上でASCII空白文字を手動で入れる。

Typstで和欧文間空白が完全にサポートされる日が早く来るといいですね:smiley:

[adobe-blank]: https://github.com/adobe-fonts/adobe-blank
[haranoaji]: https://github.com/trueroad/HaranoAjiFonts
[issue]: https://github.com/typst/typst/issues/2703
[newcm]: https://git.gnu.org.ua/newcm.git
[okumura]: https://okumuralab.org/~okumura/misc/241111.html
[wtsnjp]: https://blog.wtsnjp.com/2019/10/31/bxghost/