Typstに入門してみた話。請求書を作ってみた話。
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 には、Boolean
やInteger
、Floating-point number
、String
の他にArray
やDictionary
、For loop
、While 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 のテンプレートとは、特定の範囲のドキュメントまるごと引数として渡せるような関数のようです。
この辺は、実際の作成されているテンプレートを見ながらコツを掴んでいきたいと思います。
今回の記事はここまでとなります! ありがとうございました。
-
This tutorial does not assume prior knowledge of Typst, other markup languages, or programming. (https://typst.app/docs/tutorial/) ↩︎
-
これは Vim や Neovim がファイルを保存する際に、デフォルトではファイルを上書きせずに新しいファイル作成して置き換えるという挙動をすることによるものです。この問題について詳しくは該当する tinymist の issueをご覧ください。 ↩︎
-
https://typst.app/docs/reference/foundations/arguments/#spreading ↩︎
-
テンプレートについてはチュートリアルに詳しいです https://typst.app/docs/tutorial/making-a-template/ ↩︎
Discussion