💴

Typstに入門してみた話。請求書を作ってみた話。

2024/12/10に公開

Typst については以前から「なんか良さそう」と思ってはいたものの、なかなか手を出せずにいましたが、今回の Advent Calendar を書くことをきっかけに入門してみることにしました。
この記事では、ローカルでの環境構築から始まり、チュートリアルを読んで、簡単な請求書を作ってみた過程について記述します。

Typst については、LaTeX ユーザ向けの手引き書もあります。私の場合は、LaTeX の知識がすっかりエスケープしてしまったので、前提知識を必要としない[1]チュートリアルを読み進めていきます。

環境構築

チュートリアルではブラウザ上で編集・プレビューできるtypst.appの使用を勧めていますが、私は手に馴染んだ Vim で作業をしたいのでローカルでの環境を整えていきます。
私が普段作業している環境は Arch Linux です。Arch Linux の公式パッケージには typst がありますので導入は簡単です。また、typst の変更を監視し hot-reload するための機能や LSP としての機能がある tinymist もインストールしておきます。

sudo pacman -S typst tinymist

なお、Vim や Neovim を使用して tinymist でプレビューを表示している場合は、Vim や Neovim のデフォルトの設定ではファイルの変更が認識されない不具合があります[2]。Workaround として、以下ののように Vim の設定ファイルに書き足しておきました。

augroup MyTypst
  autocmd!
  autocmd Filetype typst setlocal backupcopy=yes
augroup END

Hello, World

Typst では markup などで書かれた表現のことをcontentと読んでいます[3]
例えば、次の align 関数を使った例では、[Hello, World]というcontentを引数に取って、右にalignされた Hello, World というcontentを出力します。

#align(
  right,
  [Hello, World]
)

また、引数のcontentだけを外に出す、次のような書き方もできます。

#align(right)[
  Hello, World
]

これらを見ると、設定が増えた時にどんどんネストが深くなっていって大変そうだなと思ったのですが、以下のような書き方も用意されています。

#set align(right)
Hello, World

次のように複数の設定を書くこともできます。

#set par(justify: true)
#set align(right)
Hello, World

#set par(justify: false)
#set align(left)
Good bye, World

このsetによる記述は、今後その関数を使う時のデフォルト引数を指定しておくという行為を概念化したものなのだそうです[4]
書きやすくなった反面、contentが状態を持ってしまっているように見えて (パーサなどを書く時に特に) 大変そうだという感想を抱きました。

スクリプト

Typst には、BooleanIntegerFloating-point numberStringの他にArrayDictionaryFor loopWhile loopなどをコード・ブロックに書くことができます[5]。ここまで扱ってきた関数の呼び出しもコード・ブロックとして書かれています。
さて、Arrayに対しては、map関数なども用意されているので、請求書を作るくらいであれば十分そうです。
ということで、さくっと作ってみましょう。

請求書作ってみた

letで請求項目 (Dictionary) の配列を定義します。

#let items = (
  (
    name: "Service Foo",
    amount: 11,
    price: 100000,
  ),
  (
    name: "Service Bar",
    amount: 4,
    price: 300000,
  ),
)

For loopで加工してtable関数の引数として渡してテーブルを作成します。..spreading operatorです[6]

#table(
  table.header(
    [詳細], [数量], [単価], [金額]
  ),
  ..for item in items {(
    item.name,
    [#{item.amount}個],
    [#{item.price}円],
    [#{item.amount * item.price}円],
  )}
)

合計金額もmap関数とsum関数が組み込みで用意されているので気持ち良く書けました。

#let tax-rate = .1
#let total-price = items.map(it => it.price * it.amount).sum(default: 0)
#let tax = total-price * tax-rate
#let total = total-price + tax

完成!

組版マークアップ言語の記事なのに、肝心の組版部分がかなりやっつけになってしまいましたが、以下のように、土台としてはおよそ満足のいくものが簡単に作成できました。

#let date = (year: 2024, month: 12, day: 10)
#let serial = 1
#let document-number = [#date.year#date.month#{date.day}-#serial]
#let due-date = (year: 2024, month: 12, day: 31)
#let tax-rate = .1
#let items = (
  (
    name: "Service Foo",
    amount: 11,
    price: 100000,
  ),
  (
    name: "Service Bar",
    amount: 4,
    price: 300000,
  ),
)
#let total-price = items.map(it => it.price * it.amount).sum(default: 0)
#let tax = total-price * tax-rate
#let total = total-price + tax

= 請求書
#v(1em)
#grid(
  columns: (4fr, 3fr),
  {
    [Hoge Fuga 御中]
    v(.1em)
    [
      〒000-0000 \
      東京都hoge区fuga 1-1
    ]
    v(1em)
    [下記の通りご請求します。]
    table(
      columns: (auto, auto, auto),
      inset: (x: 1.5em, y: .5em),
      [小計],
      [消費税],
      [合計金額],
      [#total-price 円],
      [#tax 円],
      [#total 円],
    )
    v(1em)
    [
      振込期日: #{due-date.year}年#{due-date.month}月#{due-date.day}日 \
      振込先: 😄😄😄
    ]
  },
  {
    [
      日付: #{date.year}年#{date.month}月#{date.day}日 \
      請求書番号: #document-number
    ]
    v(1em)
    [NI57721]
    v(.1em)
    [
      〒000-0000 \
      東京都example区sample 1-1
    ]
  }
)

#table(
  columns: (1fr, auto, auto, auto),
  inset: (x: 1.5em, y: .5em),
  table.header(
    [ 詳細 ], [ 数量 ], [ 単価 ], [ 金額 ]
  ),
  ..for item in items {(
    item.name,
    [ #{item.amount}個 ],
    [ #{item.price}円 ],
    [ #{item.amount * item.price}円 ],
  )}
)

実際に上記のものを請求書として使用し続ける場合は、テンプレートとして作るのが良さそうです[7]
Typst のテンプレートとは、特定の範囲のドキュメントまるごと引数として渡せるような関数のようです。
この辺は、実際の作成されているテンプレートを見ながらコツを掴んでいきたいと思います。

今回の記事はここまでとなります! ありがとうございました。


脚注
  1. This tutorial does not assume prior knowledge of Typst, other markup languages, or programming. (https://typst.app/docs/tutorial/) ↩︎

  2. これは Vim や Neovim がファイルを保存する際に、デフォルトではファイルを上書きせずに新しいファイル作成して置き換えるという挙動をすることによるものです。この問題について詳しくは該当する tinymist の issueをご覧ください。 ↩︎

  3. https://typst.app/docs/reference/foundations/content/ ↩︎

  4. https://typst.app/docs/tutorial/formatting/#set-rules ↩︎

  5. 詳しくは https://typst.app/docs/reference/syntax/ ↩︎

  6. https://typst.app/docs/reference/foundations/arguments/#spreading ↩︎

  7. テンプレートについてはチュートリアルに詳しいです https://typst.app/docs/tutorial/making-a-template/ ↩︎

GitHubで編集を提案

Discussion