Typst でも縦書きをして新年のご挨拶をしたい

<!-- # Typst でも縦書きをして新年のご挨拶をしたい -->

[Typst 0.10.0][github-typst] では縦書きをサポートしていません。しかしながら、部分的に縦書きを使いたいときもあります。

[github-typst]: https://github.com/typst/typst/releases/tag/v0.10.0 "Release Version 0.10.0 (December 4, 2023) · typst/typst"

そこで、疑似的に縦書きを可能にする方法を作ってみました。

:::note info
縦書きに見えるように出力しますが、縦書きそのものではありません。あくまで疑似的です。

今回実装した方法では、少なくとも以下のような問題を抱えています。

- 自動改行できない
- 句読点の位置が奇妙
- ゃ・ゅ・ょ・っ等の配置が奇妙
- カーニングできない
:::

## 縦書きにする方針

[`split`][split] 関数と [`clusters`][clusters] 関数を利用することで文章を 1 文字ずつに分割し、[`stack`][stack] 関数で下方向に向かって並べるようにします。

具体的には以下のようなステップを踏みます。

1. `split` 関数を使って改行 (`\n`) で分割し配列として取得
    - 配列を左方向に積み上げる (`stack`)
1. `split` 関数を使って空白 ([Zs][]) で分割し配列として取得
    - 配列を下方向に積み上げる (`stack`)
1. `clusters` 関数を使って 1 文字ずつの配列として取得
    - 特定の文字の場合は 90 度左回転
    - 1 文字ずつを下方向に積み上げる (`stack`)

[split]: https://typst.app/docs/reference/foundations/str/#definitions-split "String Type – Typst Documentation"
[clusters]: https://typst.app/docs/reference/foundations/str/#definitions-clusters "String Type – Typst Documentation"
[stack]: https://typst.app/docs/reference/layout/stack/ "Stack Function – Typst Documentation"
[Zs]: https://www.compart.com/en/unicode/category/Zs "List of Unicode Characters of Category “Space Separator”"

文字を並べているだけなので、カーニングのような小難しいことはまったく出来ません。古の組版 (?) を感じてください。（ただし、1 文字毎の間隔を指定することは出来ます）

### 縦書き内のラテン文字

縦書きの中のラテン文字は、左側が下になるような向き（90 度左回転）になります。これにも上手く対応します。

このような縦書き内で向きを変える必要のある文字は、正規表現を使って以下のように指定しておきます。

```rust
#let rotateChar = "[ー\u{FF5E}\u{301C}\p{Open_Punctuation}\p{Close_Punctuation}a-zA-Z0-9]"
```

この正規表現では次の文字を指定してます。

- 長音符
- 波ダッシュ・全角チルダ
- 開き括弧・閉じ括弧
- a-z、A-Z
- 0-9

また、これらはこれかこれ以外かを単純に [`if-else`][conditional] 文で判別します。

[conditional]: https://typst.app/docs/reference/scripting/#conditionals "Scripting – Typst Documentation"

### 行間隔・単語間隔

改行や空白がある場合、これに対応して縦書きの改行や単語間に空白を設けるようにします。

実際、これは `stack` 関数の `spacing` を利用しているだけです。

## コード

これで完成したコードは次のようになります。

<details>
<summary>コード（折りたたみ）</summary>

```rust
#let rotateChar = "[ー\u{FF5E}\u{301C}\p{Open_Punctuation}\p{Close_Punctuation}a-zA-Z0-9]"
#let vw( body, size: 13.5pt, font: "Harano Aji Gothic", weight: "regular",
        kerning: 0.3em, leading: 0.5em, spacing: 1em, ) = {
  set text(size: size, font: font, weight: weight)
  set align(right)
  stack( dir: rtl, spacing: leading,
    ..for s in body.split(regex("[\n]")) {
      // "s"
      ( // 1 ->
        stack(dir: ttb, spacing: spacing,
          ..for t in s.split(regex("[\p{Zs}]")) {
            // "t"
            ( // 2 ->
              stack( dir: ttb, spacing: kerning,
                ..for l in t.clusters() {
                  // "l"
                  ( // 3 ->
                    if regex(rotateChar) in l {
                      set align(center)
                      rotate(90deg, l)
                    } else {
                      set align(center)
                      l
                    },
                  ) // <- 3
                }
              ),
            ) // <- 2
          }
        ),
      ) // <- 1
    }
  )
}
```

</details>

Typst のドキュメント風にパラメータの説明をすると、以下のようになります。

```rust
vw(
  size: `length`
  font: `str`, `array`
  weight: `int`, `str`
  kerning: `none`, `relative`, `fraction`
  leading: `none`, `relative`, `fraction`
  spacing: `none`, `relative`, `fraction`
  `str`
)
```

| パラメータ | 説明               |    デフォルト     |
| :--------: | :----------------- | :---------------: |
|   `size`   | 文字の大きさ       |      13.5pt       |
|   `font`   | フォント           | Harano Aji Gothic |
|  `weight`  | フォントのウェイト |      regular      |
| `kerning`  | 文字と文字の縦間隔 |       0.3em       |
| `leading`  | 行間隔             |       0.5em       |
| `spacing`  | 空白の大きさ       |        1em        |

利用方法に必要な注意として、`split` 関数は文字列しか受け付けません。コンテンツを受け付けないため、途中の文章を装飾することは出来ません。

### 利用例

以下の利用例では上のコードを vw.typ として保存し、[`import`][import] で導入します。

[import]: https://typst.app/docs/reference/scripting/#modules "Scripting – Typst Documentation"

----------

複数の文章を並べる場合、縦書きのように左側に並ばないため、[`table`][table] 関数で表形式を利用します。（文章は [青空文庫][amenimo] より）

[table]: https://typst.app/docs/reference/model/table/ "Table Function – Typst Documentation"
[amenimo]: https://www.aozora.gr.jp/cards/000081/files/45630_23908.html "宮澤賢治 〔雨ニモマケズ〕"

```rust
#import "vw.typ": *
#set page(width: 15cm, height: 10cm, margin: 1cm)
#set align(center)

#let title = "雨ニモマケズ"
#let author = "宮澤 賢治"
#let main = "雨ニモマケズ
風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ
丈夫ナカラダヲモチ
慾ハナク
決シテ瞋ラズ
イツモシヅカニワラッテヰル
一日ニ玄米四合ト
味噌ト少シノ野菜ヲタベ
アラユルコトヲ
ジブンヲカンジョウニ入レズニ
ヨクミキキシワカリ
ソシテワスレズ"

#table(
  columns: (auto, 4em, 3em),
  align: (right, right+bottom, right),
  stroke: none,
  vw(main),
  vw(author),
  vw(title, kerning: 0.6em),
)
```

![雨ニモマケズ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590705/8f1d4be0-13b5-5889-a6c7-b316f8b72882.png "Typst で作った雨ニモマケズ")

句読点やラテン文字が入る良い感じの例が見つからないので、実際に試してみてください。

## 新年のご挨拶

Typst で疑似的な縦書きを実装してみました。

これを使って新年のご挨拶をしたいと思います。新年のご挨拶と言えば年賀状ですね。
と言うことで、“Typst で年賀状” を作ってみました。

<details>
<summary>年賀状（折りたたみ）</summary>

挿入されている辰の絵は [いらすとや][irasutoya] から取得しました。

[irasutoya]: https://www.irasutoya.com/2012/02/blog-post_6019.html "年賀状のイラスト「雲の上で寝る龍・オス」辰年・干支 | かわいいフリー素材集 いらすとや"

```rust
#import "vw.typ": *
#let bgColor = rgb("#f7edc6")
#let backCard = rect( width: 100mm-10mm, height: 148mm-10mm, fill: bgColor )
#set page(
  width: 100mm, height: 148mm, margin: 20pt,
  background: backCard,
)
#show image: set align(center)
#let stamp = {
  box(
    fill: gradient.linear(red, red, red, bgColor, angle: 120deg),
    width: 23pt, height: 20pt,
    outset: 5pt, inset: 2.5pt,
    radius: 5pt,
  )[#text(bgColor, size: 20pt)[*辰*]]
}

#let greeding = "謹賀新年"
#let letter = "昨年は大変お世話になりました

本年もそれなりに頑張りますので
何卒よろしくお願いします

皆さまも幸多き年となりますよう
心よりお祈り申し上げます"
#let date = "令和六年 元日  "

#image("dragon_cloud.png", height: 150pt)

#table(
  align: (left+bottom, center+bottom, center, right),
  columns: (20pt, 20pt, 140pt, 62pt),
  stroke: none,
  stamp,
  vw(date, spacing: 1.5em, font: "Noto Serif CJK JP"),
  vw(letter),
  vw(greeding, size: 40pt, kerning: 0.74em, font: "Noto Serif CJK JP", weight: "bold"),
)
```

</details>

![2024年の年賀状](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590705/2c58d2d6-86e8-8152-cde5-d7ff32399556.png "Typstで作った年賀状")

こだわりポイントは「辰」印がちょっとかすれているところです()

## 参考

- [体验Typst - 知乎](https://zhuanlan.zhihu.com/p/617680831?utm_id=0 "体验Typst - 知乎")
- [Using a loop to render blocks creates a space between them · Issue #599 · typst/typst](https://github.com/typst/typst/issues/599 "Using a loop to render blocks creates a space between them · Issue #599 · typst/typst")

Typst で縦書きをしている人がとても少ないようでした。
Twitter 上で Typst の縦書きをテーブルレイアウトで再現している人 <sup>[⧉][saitoatsushi]</sup> がいましたが、その他 <sup>[⧉][twitter-search]</sup> は見つけることが出来ませんでした。

[twitter-search]: https://twitter.com/search?q=typst%20%E7%B8%A6%E6%9B%B8%E3%81%8D "typst 縦書き - 検索 / Twitter"
[saitoatsushi]: https://twitter.com/SaitoAtsushi/status/1639977439777685504 "齊藤敦志さん: 「Typst で強引に縦書き組版を試みていた。 古の技術、テーブルレイアウトである！」 / Twitter"

# まとめ

新年のご挨拶を兼ねた Typst でも縦書きしたいでした。

2024 年もよろしくお願いいたします。