🌀

話題の組版エンジン Typst を触ってみた

2023/04/19に公開

Typst — 新しい組版エンジン

今年の3月21日、Typst というソフトウェアが public beta となり、処理系がオープンソースで公開されました。

https://typst.app

Typst is a new markup-based typesetting system for the sciences.

Typst はマークアップベースの新しい組版システムです。独自の構文を持っており、Typst で書かれた文書をオンラインサービスまたはローカル上でコンパイルすることで PDF に変換(出力)することができます。

公式サイトには「科学分野向け」「LaTeX への不満から生まれた」とあり、数式や相互参照等の機能が充実していることから、メインターゲットも LaTeX ときわめて近いところにあると考えられます。とはいえそれ自体は汎用的な組版システムなので、用途も

  • 論文・会議原稿
  • 雑誌の記事
  • 小説
  • 技術書
  • スライド・ポスター

など色々考えられます。現在は PDF 出力しかサポートしていませんが、将来的には HTML や epub など他の形式もサポートされ、さらに活用の幅が広がるかもしれません。

Typst の特色

Typst の特色は色々あります。

  • 非常に新しい。
    • 2019 年から開発とのことですから、SATySFi より後に開発が始まったということになります。
    • 処理系がオープンソース化されたのは冒頭で述べたとおり今年の3月です。
  • Rust で開発されている。
    • language server などの周辺ツールが作りやすそうです。
  • 公式ドキュメント が充実している。
    • これが一番ありがたい。

しかし、個人的に最も注目しているのはコンテンツや文書の体裁を記述するための独自構文、 Typst 言語(とここでは呼びます)の書き心地です。

Typst 言語

組版用の言語を設計する上で悩ましいのが、「文書をどの程度簡潔に記述できるようにするか」です。Markdown のように軽量マークアップ言語としての文法に寄せてしまうと、複雑なプログラムが記述しづらくなる傾向にあります。かといって一般のプログラミング言語のように冗長な書き方は文書の記述に適していません。良いバランスが求められるのです。

Typst ではどうでしょうか。以下の例を見てください。

example-1.typ
= 見出し1

これは段落です。空行は改段落を表します。
// 2つのスラッシュから始まる行はコメントです。このテキストは出力に現れません。

== 見出し2

テキストは _強調 (emphasis)_ することもできれば、
*強調 (strong emphasis)* することもできます。

- これは箇条書きです。
  - インデントは箇条書きのネストを表します。
- 先頭を `+` とすれば番号付き箇条書きとなります。

これは Typst で書かれたシンプルな文書です。Markdown や AsciiDoc を彷彿とさせる、軽量マークアップ言語に近い構文ですね。これをタイプセットすると以下のような PDF が得られます。


組版結果の例。各マークアップの出力の違いを強調するため、設定を多少入れている。

では以下の例はどうでしょう。こちらも Typst で書かれた文書です。

example-2.typ
#heading(level: 1)[見出し1]

これは段落です。空行は改段落を表します。

#heading(level: 2)[見出し2]

テキストは #emph[強調 (emphasis)] することもできれば、
#strong[強調 (strong emphasis)] することもできます。

#list(
  [
    これは箇条書きです。
    #list[インデントは箇条書きのネストを表します。]
  ],
  [
    先頭を #raw("+") とすれば番号付き箇条書きとなります。
  ],
)

先程の例と異なり、今度は # から始まるコマンドのようなものが多用されています。これは公式ドキュメントの言葉を借りると "embed a code expression into markup"、つまり関数呼び出しなどの式をマークアップ内に埋め込むための記法です。

そして、察しの良い方はすでにお気づきかもしれませんが、先程の例 (example-1.typ) とこの例 (example-2.typ) では同じ結果が得られます。example-1.typ で用いた構文には同じ効果を持つ組み込み関数がそれぞれ存在しており、# から始まる記法でそれらを呼び出せるのです。

マークアップの種類 構文 関数
強調 _text_ #emph[text]
より強い強調 *text* #strong[text]
見出し = text #heading(level = ...)[text]
番号無し箇条書き - list #list(...children)
生のテキスト `text` #raw("text")

なお、構文と対応する関数の網羅的な一覧は Syntax – Typst Documentation に記されています。

関数による記述は専用の構文に比べ冗長ですが、その分柔軟です。たとえば

#list(
  indent: 2em,
  [
    これはインデントが `2em` の箇条書きです。
  ],
  [
    先頭を #raw("+") とすれば番号付き箇条書きとなります。
  ],
)

と書くことで、箇条書きのインデントを 2em (フォントサイズの2倍)に変更できます。このように Typst の関数はオプション引数を取ることができ、その値を変えることで様々な挙動をカスタマイズできるのです。
それだけではありません。 #set 構文を使えば、オプション引数のデフォルト値を変更することもできます。たとえば

#set list(indent: 2em)

#list(
  [
    これはインデントが `2em` の箇条書きです。
    #list[`#set` はネストされた `#list` にも影響を与えます。]
  ],
  [
    先頭を #raw("+") とすれば番号付き箇条書きとなります。
  ],
)

とすれば…何が起きるか大体想像がつきますよね。はい、そういうことなんです。#set によるデフォルト値の変更はマークアップの記法を用いた場合にも適用されるため、

#set list(indent: 2em)

- これはインデントが `2em` の箇条書きです。
  - `#set` はネストされた `#list` にも影響を与えます。

と書いても、やはり期待する通りの動きになります。特定の箇条書きのみ影響を与えたければ

#[
  #set list(indent: 2em)

  - これはインデントが `2em` の箇条書きです。
    - `#set` はネストされた `#list` にも影響を与えます。
]

- これはインデントがデフォルト設定の箇条書きです。

のように、ブロックの中に入れてスコープを切ると良いでしょう。マークアップ指向で文書を記述するとき、 #set は体裁を変更するための強力なツールとなるのです。

Typst 言語には他にも、以下のような構文や関数が用意されています。

  • #if#for などを使えば、マークアップ上で条件分岐や繰り返しを実現できる
  • #let で変数や関数を定義できる
  • #show 構文でマークアップの挙動をより細かくカスタマイズできる

詳しい説明は公式ドキュメントに譲ります。

Typst の組版結果の例

それでは、試しに Typst で書いてみた少し長めの例をお見せします。折角なので、昔の講義ノートを引っ張り出し、少し数式多めの文書を書いてみました。


Typst で書いてみた文書。数式多め。内容の正確さは保証しません。

これを実現するコードが以下です。一部紹介していない関数や構文もありますが、雰囲気でなんとなく読めるのではないでしょうか。

// --------- ちょっとした設定 ---------

// フォント周り
#set text(font: "Noto Serif CJK JP")
#show emph: set text(font: "Noto Sans CJK JP")
#show strong: set text(font: "Noto Sans CJK JP", fill: red)

// 段落での両端揃えを有効化・行送りの長さを指定
#set par(justify: true, leading: 0.75em)

// 箇条書きと別行立て数式の設定
#set list(indent: 0.5em)
#set enum(numbering: "(1)")
#set math.equation(numbering: "(1)")

// theorem 用カウンタの定義
#let theorem-number = counter("theorem-number")

// theorem 関数の定義。コマンドみたいに使える
#let theorem(title: none, kind: "定理", body) = {
  let title-text = {
    if title == none {
      emph[#kind 2.#theorem-number.display()]
    } else {
      emph[#kind 2.#theorem-number.display() 【#title】]
    }
  }

  box(stroke: (left: 1pt), inset: (left: 5pt, top: 2pt, bottom: 5pt))[
    #title-text #h(0.5em)
    #body
  ]

  theorem-number.step()
}

// 数式で用いるエイリアス($\mathcal{F}$ 的なやつ)
#let cF = $cal(F)$

// 以降のテキストで現れる句読点を全角カンマピリオドに置換する。そんなこともできるの…
#show "、": ","
#show "。": "."

// --------- ここから本文のマークアップ ---------

#theorem(kind: "定義", title: [$sigma$-加法族])[
  $Omega$ の部分集合族 $cF$ が以下の性質を満たすとき、 $Omega$ を $sigma$-加法族という。

  + $Omega in cF$
  + $A in cF ==> A^c in cF$
  + $A_1, A_2, dots in cF$ に対して以下のことが成り立つ(_$sigma$-加法性、完全加法性、加算加法性_):
    $
    union.big_(i=1)^infinity A_i in cF
    $
]

$A subset Omega$ に「確率」を定めたい。矛盾なく「確率」が定まる集合をあらかじめ決めておきたい。
それが $sigma$-加法族である。
$Omega$ と $cF$ の組 $(Omega, cF)$ を#strong[可測空間]という。
また、$cF$ の元を#strong[可測集合](または事象、Event)という。

#theorem(kind: "定義", title: [確率測度])[
  $(Omega, cF)$ を可測空間とする。 $cF$ 上の関数 $P$ が次を満たすとき、これを#strong[確率測度]という。

  - $0 <= P(A) <= 1 #h(0.5em) (forall A in cF)$
  - $P(Omega) = 1$
  - $A_1, A_2, dots in cF$ が $A_i sect A_j = nothing #h(0.25em) (forall i != j)$ のとき、
    次が成り立つ($sigma$-加法性、完全加法性):
    $
    P(union.big_(i=1)^infinity A_i) = sum_(i=1)^infinity P(A_i)
    $
]

$P$ が $(Omega, cF)$ の確率測度のとき、 $(Omega, cF, P)$ を#strong[確率空間]という。

#theorem(kind: "例", title: [一定時間に到着するメールの数])[
  $Omega = {0, 1, 2, dots}$ で、
  $
  P(A) = sum_(omega in A) (lambda^omega)/(omega!) e^(-lambda)
  $
  とすると、これも確率測度になっている($A$ は強度 $lambda$ の Poisson 過程に従うという)。
]

$Omega$ が加算無限の場合、 $cF = 2^Omega$ を考えておけば問題ない。
$0 <= h(omega) <= 1$, $sum_(omega in Omega) h(omega) = 1$ となるような $h$ を用いて
$P(A) = sum_(omega in A) h(omega)$ とおけば、 $P$ は確率測度となる。
この $h(omega)$ のことを、確率質量関数という。

ページの設定は一切行っておらず、クラスファイル等を読み込むことすらしていませんが、すでに一定の品質の原稿ができています。数式についても、上の例に出てくるようなシンプルなものであれば LaTeX で組んだ結果と比較してさほど違和感はありません。大学の講義ノートやレポート程度の用途なら、すでに実用に足るレベルではないでしょうか。

実際に触ってみた感想

  • 構文仕様が面白い。
    • マークアップとプログラミングの両側面を両立するとなると様々な問題が生じがち(括弧の種類が足りないとか)ですが、構文にある程度の一貫性をもたせながら器用に解消している印象を持ちました。
    • 数式の構文も、個人的には TeX/LaTeX より Typst のほうが好み。
    • ただし、パーサを書くのは大変そうです。
      • マークアップ記法と拡張性を両立するというコンセプト的には仕方ないことではあります。
      • マークアップ・数式モード・コードモードの3つのモードを持ち、マークアップモードではオフサイドルールが採用されています。
    • これってインラインの要素なの?ブロックの要素なの?というのがよく分からないことがあります。これはまだ自分が慣れていないだけかもしれません。
  • 思った以上にいろいろなことができる。
    • 言語仕様的にも、組版エンジン的にも。
    • 開発者が日本人でないため和欧混植はどうなるだろうと思っていましたが、特に工夫せずとも動きました。RTL (right-to-left) な言語にも対応しているようです。
  • LSP での静的解析もある程度できていてすごい。
    • Typst にも型の概念はあるものの、おそらく静的型付けではないと思われます(自信なし)。
    • その点で言えばポテンシャルは SATySFi のほうが上かもしれません。たとえばユーザ定義関数のシグニチャの型を判断して補完結果を変える…みたいなことは現状の Typst だと難しいのではないかと思います。

おわりに

本記事で少しでも Typst に興味を持った方は、実際に試してみることをおすすめします。Typst はすでにオンラインでの実行環境を提供しています。ログインが必要ですが、オンライン上ではシンタックスハイライトや補完が効くため、実行環境を作るには最も楽かもしれません。

https://typst.app

なお私は Neovim 上で開発するのが性に合っているため、typst コマンドをインストールしてローカルで実行しています。インストール方法は公式リポジトリの README を参照してください。

https://github.com/typst/typst

すでに language server も存在しており、実際に Neovim (coc.nvim) 上で動くことを確認しています。開発も活発なので、どんどん新しい機能が追加されていくことでしょう。

https://github.com/nvarner/typst-lsp

今後の開発次第では、まさに組版界隈にとっての台風の目となるかもしれません。皆さんもぜひ Typst を使ってみてください!

Discussion