Typst におけるデータと制御

Typst は CSV や JSON などのテキストベースのデータ(ファイル)を読み込んで使うことができる。 簡単な例をいくつか挙げてみる。

CSV データの読み込みと表示

以下の内容の CSV ファイルがあるとする。

"日付","曜日","名称"
"2025年5月3日","土","憲法記念日"
"2025年5月4日","日","みどりの日"
"2025年5月5日","月","こどもの日"
"2025年5月6日","火","休日"

これを読み込んで表にすることを考える。 Typst のコードはこんな感じ。

#show raw: body => {
    set text(font: (
      (
        name: "Inconsolata",
        covers: "latin-in-cjk",
      ),
      "Noto Sans CJK JP"
    ))
    body
}

#let holidays = csv(
  "./holidays.csv",
  delimiter: ",",
  row-type: dictionary,
)

#holidays

フォントの指定については今回はスルーで1(笑) CSV データの読み込みには raw 関数を使う。 今回のように1行目がヘッダ情報になっている場合は row-typedictionary を指定する。 ヘッダ情報がない場合は既定の array でOK。

これを PDF に出力すると以下のような内容になる。

CSV データの読み込み

見ての通り連想配列(dictionary)の配列(array)という構造になっている。

次にこれをヘッダ情報とデータに分離する。 Typst のコードはこんな感じ。

#show raw: body => {
    set text(font: (
      (
        name: "Inconsolata",
        covers: "latin-in-cjk",
      ),
      "Noto Sans CJK JP"
    ))
    body
}

#let holidays = csv(
  "./holidays.csv",
  delimiter: ",",
  row-type: dictionary,
)
#let header = holidays.first().keys()
#let data = holidays.map(holiday => holiday.values())

#header

#data

PDF への出力結果は以下の通り。

ヘッダとデータを分離

data は2次元配列になっている点に注意。 一応,元データの並び順のままヘッダ情報もデータも取れるんだね。 array にはコンテナ操作ではお馴染みの filter, map, fold といったメソッドが使える。 ありがたや。

これで CSV データは取れたので table へ展開してみる。

#set text(font: "NOTO Serif CJK JP", lang: "ja")

//関数定義
#let tableOfHolidays(path) = {
  let holidays = csv( //CSV ファイルの読み込み
    path,
    delimiter: ",",
    row-type: dictionary,
  )

  if holidays.len() > 0 { //データがある場合のみテーブルを表示
    let header = holidays.first().keys() //ヘッダ情報の抽出
    table(
      columns: header.len(), //ヘッダ情報の要素数
      align: header.map(it => {
          if it == "日付" {
              right
          } else if it == "曜日" {
              center
          } else {
              left
          }
        }
      ), //ヘッダ情報の名前によって文字列の寄せを設定
      fill: (x, y) => if y == 0 {
        green.lighten(80%)
      }, //ヘッダ部の背景色を設定
      table.header(..header.map(it => {
          set text(font: "NOTO Sans CJK JP", weight: "bold")
          it
      })), //ヘッダ情報,文字コードも併せて設定している
      ..holidays.map(holiday => holiday.values()).flatten() //データを一次元のデータの並びに展開
    )
  }
}

//CSV ファイルの読み込んでテーブルを表示
#tableOfHolidays("./holidays.csv")

まず tableOfHolidays 関数を定義して CSV ファイルへのパスを引数とする(CSV 形式の文字列でも可)。 tableOfHolidays 関数内では CSV ファイルからデータを取得して table へ展開している。 最後に tableOfHolidays 関数に CSV ファイルへのパスを渡して実行する。

配列に対する flatten 関数は多次元配列を一次元配列に展開する。

配列の頭に付いている .. は配列を要素の並びに展開する。 関数の引数で min(..nums) みたいな感じでよく使われる。

あとはヘッダ部の装飾のためにごちゃごちゃ書いているが,詳細は割愛する。 そんなもんと思って眺めていただければ(笑)

PDF への出力結果は以下の通り。

CSV からテーブル生成

まぁ,こんなもんかな。

JSON データを読み込んでカレンダーを作ろう

もうひとつ。 練習問題としてカレンダーを作ってみる。

Typst には日時情報を操作する型として datetime があるのだが,今回は「外部データを読み込んで使う」のが目的なので使わない。

ある月のカレンダーを組む際に必要な情報としては以下のものがあればいいだろう。

  • 月初日の曜日(060 が日曜日)
  • 月の最終日

datetime では「月の最終日」を取得するのが面倒くさいんだよな2。 愚痴はともかく,まずは #let calendar(year, month, first_weekday, lastday) = { ... } という関数を定義してみる。

#set text(font: "NOTO Serif CJK JP", lang: "ja")

//カレンダーを作成
#let calendar(year, month, first_weekday, lastday) = {
  let days = ()
  let i = 0
  while i < first_weekday { //初日の曜日まで空白を追加
    days.push("")
    i = i + 1
  }
  days = days + range(1, lastday + 1).map(day => { //日付を追加
    [#day]
  })
  //カレンダーを作成
  table(
    stroke: (x, y) => if y == 1 {//罫線を設定
      (bottom: 0.7pt + black)
    },
    align: (x, y) => ( //文字の位置を設定
      if y > 1 { right }
      else { center }
    ),
    columns: 7, //列数を設定
    table.header( //ヘッダーを設定
      table.cell( //年月を設定
        colspan: 7,
        [
          #set text(font: "NOTO Sans CJK JP", weight: "bold")
          #year  #month 
        ]
      ),
      ..(text(red)[], [], [], [], [], [], text(blue)[]).map(it => { //曜日を設定
        set text(font: "NOTO Sans CJK JP", weight: "bold")
        it
      })
    ),
    ..days.enumerate(start:0).map(it => {
      if calc.rem(it.at(0), 7) == 0 { //日曜日の場合
        table.cell(
          [
            #set text(red)
            #it.at(1)
          ]
        )
      } else if calc.rem(it.at(0), 7) == 6 { //土曜日の場合
        table.cell(
          [
            #set text(blue)
            #it.at(1)
          ]
        )
      } else { //その他の場合
        table.cell(
          [#it.at(1)]
        )
      }
    }),
  )
}

#calendar(2025, 5, 4, 31) //2025年5月のカレンダーを表示

実際に曜日を考慮した日付情報を生成している部分を強調している。 他はほぼテーブルの装飾のためのコードである。 最後の行で具体的な値を与えて calendar 関数を呼び出しカレンダーを表示している。

これの組版結果は以下の通り。

カレンダーを生成(2025年5月)

んー。 こんなもんかな。

次は1月から12月までの年間カレンダーを作ってみよう。

要は作成した calendar 関数を12回呼び出せばいいのだが,必要な情報をいちいち手入力するのも不毛なので,必要なデータを JSON ファイルから取得するよう変更する。 JSON ファイルの作成は Go で以下のように組んでみた。

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

// Month represents a calendar month with its associated year, month number,
// the first weekday of the month, and the last day of the month.
// Year is the year of the month.
// Month is the month number (1-12).
// FirstWeekday is the weekday of the first day of the month (0-6, where 0 is Sunday).
// Lastday is the last day of the month.
type Month struct {
    Year         int `json:"year"`
    Month        int `json:"month"`
    FirstWeekday int `json:"first_weekday"`
    Lastday      int `json:"lastday"`
}

func main() {
    year := 2025
    months := make([]Month, 0, 12)
    for month := 1; month <= 12; month++ {
        m := Month{
            Year:         year,
            Month:        month,
            FirstWeekday: int(time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).Weekday()),
            Lastday:      time.Date(year, time.Month(month+1), 0, 0, 0, 0, 0, time.UTC).Day(),
        }
        months = append(months, m)
    }
    b, err := json.Marshal(months)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    fmt.Println(string(b))
}

このコードの実行結果は以下の通り(途中を端折っている)。

$ go run months.go | jq .
[
  {
    "year": 2025,
    "month": 1,
    "first_weekday": 3,
    "lastday": 31
  },
  {
    "year": 2025,
    "month": 2,
    "first_weekday": 6,
    "lastday": 28
  },

  ...

  {
    "year": 2025,
    "month": 12,
    "first_weekday": 1,
    "lastday": 31
  }
]

この出力を months.json ファイルにリダイレクトすればOK。

Typst のコードについては calendar 関数を呼び出してる部分を以下のように書き換える。

#{
  let calendars = ()
  for month in json("./months.json") { //月ごとにカレンダーを作成
    calendars.push(calendar(month.year, month.month, month.first_weekday, month.lastday))
  }
  //カレンダーを3列×4行で表示
  grid(
    stroke: none,
    gutter: 0.5em,
    columns: (1fr, 1fr, 1fr),
    rows: (1fr, 1fr, 1fr, 1fr),
    ..calendars,
  )
}

ここでは table ではなく grid を使っている。 機能的には両者に殆ど違いはないが,ページ内をいくつか仕切って配置するという用途であれば grid を使ったほうがいいだろうか。

組版結果は以下の通り。

年間カレンダーを生成(2025年)

次。 この年間カレンダーに対して祝日・休日の日に色を付けてみよう。

祝日・休日データの収集についても Go で以下のコードを組んでみる。

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"

    "github.com/goark/koyomi"
    "github.com/goark/koyomi/value"
)

// Holiday represents a holiday with its date and title.
// Year is the year of the holiday.
// Month is the month of the holiday (1-12).
// Day is the day of the holiday (1-31).
// Weekday is the day of the week of the holiday.
// Title is the name or description of the holiday.
type Holiday struct {
    Year    int          `json:"year"`
    Month   int          `json:"month"`
    Day     int          `json:"day"`
    Weekday time.Weekday `json:"weekday"`
    Title   string       `json:"title"`
}

func main() {
    start, _ := value.DateFrom("2025-01-01")
    end, _ := value.DateFrom("2025-12-31")
    td, err := os.MkdirTemp(os.TempDir(), "blog")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    defer func() { _ = os.RemoveAll(td) }()
    k, err := koyomi.NewSource(
        koyomi.WithCalendarID(koyomi.Holiday),
        koyomi.WithStartDate(start),
        koyomi.WithEndDate(end),
        koyomi.WithTempDir(td),
    ).Get()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    holidays := make([]Holiday, 0, len(k.Events()))
    for _, e := range k.Events() {
        holidays = append(holidays, Holiday{
            Year:    e.Date.Year(),
            Month:   int(e.Date.Month()),
            Day:     e.Date.Day(),
            Weekday: e.Date.Weekday(),
            Title:   e.Title,
        })
    }
    b, err := json.Marshal(holidays)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    fmt.Println(string(b))
}

祝日・休日データの取得は拙作 github.com/goark/koyomi パッケージを使って国立天文台から取得している。

このコードの実行結果は以下の通り(途中を端折っている)。

$ go run holidays.go | jq .
[
  {
    "year": 2025,
    "month": 1,
    "day": 1,
    "weekday": 3,
    "title": "元日"
  },
  {
    "year": 2025,
    "month": 1,
    "day": 13,
    "weekday": 1,
    "title": "成人の日"
  },

  ...

  {
    "year": 2025,
    "month": 11,
    "day": 24,
    "weekday": 1,
    "title": "休日"
  }
]

この出力を holidays.json ファイルにリダイレクトすればOK。

Typst のコードについては calendar 関数周りを以下のように書き換える。

//祝日・休日の取得
#let holidays = json("./holidays.json")

//指定した年月日が祝日・休日かどうかを判定
#let containHoliday(year, month, day) = {
  holidays.find(holiday => {
    holiday.year == year and holiday.month == month and holiday.day == day
  }) != none
}

//カレンダーを作成
#let calendar(year, month, first_weekday, lastday) = {
  let days = ()
  let i = 0
  while i < first_weekday { //初日の曜日まで空白を追加
    days.push("")
    i = i + 1
  }
  days = days + range(1, lastday + 1).map(day => { //日付を追加
    [#day]
  })
  //カレンダーを作成
  table(
    stroke: (x, y) => if y == 1 {//罫線を設定
      (bottom: 0.7pt + black)
    },
    align: (x, y) => ( //文字の位置を設定
      if y > 1 { right }
      else { center }
    ),
    columns: 7, //列数を設定
    table.header( //ヘッダーを設定
      table.cell( //年月を設定
        colspan: 7,
        [
          #set text(font: "NOTO Sans CJK JP", weight: "bold")
          #year  #month 
        ]
      ),
      ..(text(red)[], [], [], [], [], [], text(blue)[]).map(it => { //曜日を設定
        set text(font: "NOTO Sans CJK JP", weight: "bold")
        it
      })
    ),
    ..days.enumerate(start:0).map(it => {
      let day = it.at(0)-first_weekday+1 //日付
      let hflag = day > 0 and day <= lastday and containHoliday(year, month, day) //祝日・休日かどうか
      if calc.rem(it.at(0), 7) == 0 { //日曜日の場合
        if hflag { //祝日・休日の場合
          table.cell(
            fill: red.lighten(90%),
            [
              #set text(red)
              #it.at(1)
            ]
          )
        } else { //祝日・休日でない場合
          table.cell(
            [
              #set text(red)
              #it.at(1)
            ]
          )
        }
      } else if calc.rem(it.at(0), 7) == 6 { //土曜日の場合
        if hflag { //祝日・休日の場合
          table.cell(
            fill: red.lighten(90%),
            [
              #set text(blue)
              #it.at(1)
            ]
          )
        } else { //祝日・休日でない場合
          table.cell(
            [
              #set text(blue)
              #it.at(1)
            ]
          )
        }
      } else { //その他の場合
        if hflag { //祝日・休日の場合
          table.cell(
            fill: red.lighten(90%),
            [#it.at(1)],
          )
        } else { //祝日・休日でない場合
          table.cell(
            [#it.at(1)],
          )
        }
      }
    }),
  )
}

組版結果は以下の通り。

年間カレンダーを生成(2025年)

こんな感じで Typst のコードモードには得手も不得手もあるが,ある程度データを整えて与えてあげればそこそこの制御ができそうである。 ぶっちゃけ $\mathrm{\TeX}$/$\mathrm{\LaTeX}$ のマクロは触る気にもならないが Typst のコードモードは今どきのスクリプト言語が操れる人なら違和感少なくイケそうな気がする。

データファイルをコマンドラインで指定する

前節でつくった年間カレンダーはデータファイル名をコードに埋め込んでいるが,これをコマンドラインで指定できるようにしてみる。

Typst コード側は sys を使って以下のようにコマンドラインの情報を読み込むように書き換える。

//祝日・休日の取得
#let hfile = "./holidays.json"
#if "holidays" in sys.inputs {
	hfile = sys.inputs.at("holidays")
}
#let holidays = json(hfile)

calendar 関数は変更がないので割愛する。

#{
	let months = "./months.json"
	if "months" in sys.inputs {
		months = sys.inputs.at("months")
	}
	let calendars = ()
	for month in json(months) { //月ごとにカレンダーを作成
		calendars.push(calendar(month.year, month.month, month.first_weekday, month.lastday))
	}
	//カレンダーを3列×4行で表示
	grid(
		stroke: none,
		gutter: 0.5em,
		columns: (1fr, 1fr, 1fr),
		rows: (1fr, 1fr, 1fr, 1fr),
		..calendars,
	)
}

一方,コマンドライン側は以下のように指定する。

$ typst compile --input holidays=holidays2025.json --input months=months2025.json calendar5.typ

これで holidays.jsonmonths.json ではなく holidays2025.jsonmonths2025.json を読み込む。 --input オプションで指定しない場合はデフォルトのファイル名を使う。

コマンドラインで変数を指定する方法については「変数をコマンドライン引数で指定する」で少し詳しく紹介している。

余談だが

今回も VS Code 上で作業しているのだが,コーディングに関しては GitHub Copilot に大変お世話になっている。 Go のコードに関してはほぼ完璧に働いてくれるのだが, Typst のコードに関しては,どうも TypeScript と混乱してるっぽく,しょっちゅう嘘をついてくれるのが困りものである(笑)

データと制御が分離しやすくコードが(比較的)書きやすいというのは Typst の利点だと思う。 これなら業務にも組み込みやすいのではないだろうか。

ブックマーク

ブックマークは「Typst に関するブックマーク」にてまとめています。

参考文献

photo
Typst完全入門: LaTeXより簡単、Markdownより強力、美しいドキュメント作成術
doitsu (著)
2024-12-08 (Release 2024-12-08)
Kindle版
B0DPXBNTRS (ASIN)
評価     

マークアップ言語および組版ツールである Typst についての解説。 Kindle 版のみの提供。固定レイアウトではないためレイアウトが崩れまくって読みにくい。この手の技術解説書は固定レイアウトの Kindle 版か,いっそ PDF で出してほしい。でも Typst についてまとまった解説のある日本語の本は他に見当たらなかったのでありがたい。

reviewed by Spiegel on 2025-02-20 (powered by PA-APIv5)

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
丸善出版 2016-06-20
単行本(ソフトカバー)
4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
評価     

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!

reviewed by Spiegel on 2016-07-13 (powered by PA-APIv5)

photo
実用 Go言語 ―システム開発の現場で知っておきたいアドバイス
渋川 よしき (著), 辻 大志郎 (著), 真野 隼記 (著)
オライリージャパン 2022-04-22
単行本(ソフトカバー)
4873119693 (ASIN), 9784873119694 (EAN), 4873119693 (ISBN)
評価     

版元のデジタル版を購入。 Go で躓きやすい点を解説していくのが最初の動機らしい。「◯◯するには」を調べる際にこの本を調べるといいかも。

reviewed by Spiegel on 2022-10-26 (powered by PA-APIv5)


  1. データのダンプ表示時のフォント指定については「Typst のドキュメント要素」の raw の説明を参照のこと。 ↩︎

  2. datetime を使ってカレンダーを生成するバージョンも置いておく。詳しい説明は割愛するが「月の最終日」は2月以外固定なので,固定のテーブルを作って,グレゴリオ暦の閏年ルールで閏年か否かを判定して2月の最終日を調整している。さして面倒でもなかったか(笑) ↩︎