初学者が Typst 0.13.0 の段落の話を理解しようとした

これまでLaTeXを利用していましたが、数週間前にTypstというツールの存在を知りました。実際、LaTeXについてはあまり理解していないまま使用しており、TypstはLaTeXよりも表記が簡潔で、コンパイルなしで利用できるとのことで、Typstでの文書作成を始めることにしました。

しかし、LaTeXでも必要な部分だけを取り入れて文書を作成しており、Typstでも適当に文章を作成していたため、当然のことながら多くの不明点が生じました。その後、さらにアップデートが行われ、段落に関する新しい設定が追加されたとのことです。

そこで、公式ドキュメントをしっかりと読み込み、挙動を確認し、その結果を記録することで理解を深めたいと考え、この記事を作成しました。

<!---------------------------------------- 理解をする ---------------------------------------->

# 公式ドキュメントの個人的な理解
0.13.0の公式ドキュメントを雑に翻訳してみました。

https://typst.app/docs/reference/model/par/



## 段落

段落は**インラインレベル**の要素を集めたものであり、**インラインレベル**の要素に含まれるものは

+ 文章
+ 水平方向の空き : `h(長さ)`
+ box関数 : `box()`
+ インライン数式 : `$数式$`

などがある。
段落を分ける方法としては空白行を入れるか`parbreak()`を入れる。

段落の途中で `block()` などの**ブロックレベル**のコンテンツを入れると段落は中断される。**ブロックレベル**のコンテンツは

+ ~~2つ以上の段落~~ → `parbreak()` 要素を含む文章（3/9編集、ご指摘がありました。）
+ block関数 : `block()`
+ place関数 : `place()`
+ 独立した数式 : `$ 数式 $`

などがある。

:::note warn
3/9追加
空白行や `parbreak()` などが含まれない文章は段落とみなされない。
空白行や `parbreak()` などが含まれる文章になると文章全体が段落とみなされ**ブロックレベル**のコンテンツになる。
:::

`par()`は主に`set par()`の形で用いられるが `par()[]` などといった形で `[]` の中に段落を入れることができるが **ブロックレベル**のコンテンツを入れることができない。

:::note info
`par()[]` は `par(引数, [])` `par(引数, "")` といった形でも表せるますがそれぞれの記法のメリットは未だにわかりません...
:::



## `box()` と `block()`

`box()` の中に `block()` などの**ブロックレベル**のコンテンツを入れるすることで文章の途中に**ブロックレベル**のコンテンツを入れることができる。
逆に `block()` の中に `box()` などの**インラインレベル**の要素を入れることで段落の外に記載できる。次のセクションの「正式な段落」で普通に段落を入れた時との違いが説明されている。

:::note info
「正式な段落」という表記は「[Typst 0.13.0 の内容を早めに深堀り](https://zenn.dev/monaqa/articles/2025-02-20-typst-updates-v0-13#%F0%9F%92%A5-%E3%80%8C%E6%AD%A3%E5%BC%8F%E3%81%AA%E6%AE%B5%E8%90%BD-(proper-paragraph)%E3%80%8D%E3%81%A8%E3%81%84%E3%81%86%E6%A6%82%E5%BF%B5%E3%81%AE%E8%BF%BD%E5%8A%A0)」を参考にしました。
:::



## 段落の条件

段落は**インラインレベル**の内容を集めたものであるが、見出しやキャプションといった段落に含めるべきではないものもある。
ルールとして

+ 何にも属していないテキストは全て段落に含まれる
+ ブロックなどに含まれているテキストは、その中に**ブロックレベル**のコンテンツが含まれている場合にのみ段落になる
+ **インラインレベル**のコンテンツだけだと段落は作られない

打ち込んでいる最中、それが段落になるのかはすぐにはわからない。

+ `set par()` の内容( `first-line-indent` など)は正式な段落にのみ適用
+ ...

このルールを元に段落をカスタマイズすることができる。

:::note info
スクリーンリーダーやHTMLの話が出てきましたが、現状使う予定がないため割愛しました。
:::


<!---------------------------------------- 試してみる ---------------------------------------->

# 試してみた

実際に書いてみて挙動を確認しました。
普段はVS Codeを使用していますが、試験的にWebアプリ版で作成してみました。



## 段落を変えようとしてみた
空白行や `parbreak()` を入れると段落が変わったと認識されているみたいです。 `\` を入れると改行はするが段落は変わりませんでした。
`box()` は**インラインレベル**の要素のため文章中に入れても段落は変更されません。その一方で `block()` は**ブロックレベル**のコンテンツであるため段落が中断され、 再開後は段落が変わりました。（正直 `box()` も `block()` もよくわからないまま適当に使っていました...）
`par()[]` の形で1つの段落は書けましたが、2段落目を書き始めるとエラーを吐いたので段落は2つ以上あると**ブロックレベル**のコンテンツとみなされてるのでしょう。これは個人の推測なのでテキトー言っていたらすみません。


<details><summary>改行などの挙動</summary>

```test.typ
#set text(lang: "ja", font: "Harano Aji Mincho", 10pt)
// 段落が変わったときに一文字分インデントされるようにする
#set par(first-line-indent: (amount:1em, all: true))

吾輩は猫である。名前はまだ無い。

どこで生れたかとんと見当がつかぬ。 #parbreak() 何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。 \ 吾輩はここで始めて人間というものを見た。
```

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/5e248a5a-b6c1-41a6-bb81-6d97da560823.png)
</details>



<details><summary>box・blockの挙動</summary>

```test2.typ
#set text(lang: "ja", font: "Harano Aji Mincho", 10pt)
// 段落が変わったときに一文字分インデントされるようにする
#set par(first-line-indent: (amount:1em, all: true))

吾輩は猫である。名前はまだ無い。

どこで生れたかとんと見当がつかぬ。#box(rect(width: 50%, height: 1em, fill:gray))何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。 #block(rect(width: 50%, height: 1em, fill:gray))吾輩はここで始めて人間というものを見た。
```

![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/a9a15dd7-9e38-4238-bfbc-8bbb07d2154f.png)
</details>



<details><summary>parの別記法</summary>

```test3.typ
#set text(lang: "ja", font: "Harano Aji Mincho", 10pt)
// 段落が変わったときに一文字分インデントされるようにする
#set par(first-line-indent: (amount:1em, all: true))

吾輩は猫である。名前はまだ無い。

どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。

#par(hanging-indent: 1em)[
  吾輩は猫である。名前はまだ無い。
  どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
]
```

![test3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/bb0923cb-8aa4-45df-86c0-99d03ce2f2c0.png)
</details>



## boxの中にブロックレベルのコンテンツ、blockの中にインラインレベル要素を入れてみた

インライン数式とブロック数式を用いて試してみるとインライン数式が段落から外れたり、ブロック数式を文章の途中に入れることはできました。そのままだとレイアウトが終わっているのでなんらかの調整が必要そう。

<details><summary>実際の挙動</summary>

```test4.typ
#set text(lang: "ja", font: "Harano Aji Mincho", 10pt)
// 段落が変わったときに一文字分インデントされるようにする
#set par(first-line-indent: (amount:1em, all: true))

sinc関数とは、正弦関数をその変数で割って得られる関数であり$sinc(x) = (sin x)/x$と表される。

sinc関数とは、正弦関数をその変数で割って得られる関数であり$ sinc(x) = (sin x)/x $と表される。

sinc関数とは、正弦関数をその変数で割って得られる関数であり#block($sinc(x) = (sin x)/x$)と表される。

sinc関数とは、正弦関数をその変数で割って得られる関数であり#box($ sinc(x) = (sin x)/x $)と表される。
```

![test4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/3d83a936-de3f-4831-97a1-e263b17f9cc9.png)
</details>




## 正式な段落

上記の「[Typst 0.13.0 の内容を早めに深堀り](https://zenn.dev/monaqa/articles/2025-02-20-typst-updates-v0-13#%F0%9F%92%A5-%E3%80%8C%E6%AD%A3%E5%BC%8F%E3%81%AA%E6%AE%B5%E8%90%BD-(proper-paragraph)%E3%80%8D%E3%81%A8%E3%81%84%E3%81%86%E6%A6%82%E5%BF%B5%E3%81%AE%E8%BF%BD%E5%8A%A0)」でわかりやすく解説されています。


# 自分がやりたかったこと

自分がTypstを使う目的は数学の教材の作成です。
下記の画像(とある県の高校入試)のようなレイアウトを作ろうとしたときに

+ 見出しを大〇問に設定したい
+ それ以降は一文字分ずつインデントしていきたい
+ 最初の段落はラベルと一文字分、改行は左端から一文字分、新しい段落は左端から二文字分開けたい

という問題が発生しました。

![SnapCrab_NoName_2025-3-4_7-35-40_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/61afa69c-c7e7-4d29-b26c-b3fad346a468.png)

問題番号の書式は後で設定し直すとして、まずは空白の制御を試みます。
下記の適当に作った問題を元に見出し、`enum()` の設定を変えてみます。

![SnapCrab_NoName_2025-3-4_7-46-24_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/a58587e5-8a07-4f32-8240-95dd78586643.png)

※ `Ana()` は穴埋めのboxを高さ5mmで適当に作りました。


## デフォルト

<details><summary>コード</summary>

```test5.typ
#set text(lang: "ja", font: "Harano Aji Mincho", 10pt)
#show math.equation: set text(
  lang: "ja",
  font: (
    (name: "New Computer Modern Math", covers: "latin-in-cjk"),
    "Harano Aji Mincho"
  ),
  size: 10pt
)
// 段落が変わったときに一文字分インデントされるようにする
#set par(first-line-indent: (amount:1em, all: true))
// 正式な段落を確かめる
#show par: set text(fill: blue)
#set heading(numbering: "1")
#set enum(
  numbering: "(1)",
  spacing: 1.2em
)
```
</details>

![test5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/27abb93a-a48e-4d60-9f7f-56b5395481d6.png)

問題点として

+ 見出しの空白が目標と違いすぎる
+ 小問のベースラインがおかしい
+ ボックスレベルの数式が終わった後の段落が新しい段落とみなされ、インデントが適用されている

ということがあげられます。
知識のない自分はとりあえず見出しを `#set heading()` や `#show heading()` でなんとかならないか試してみます。
また、小問は[List and enum markers are not aligned with the baseline of the item's contents](https://github.com/typst/typst/issues/1204)に載っている解決策をそのまま流用することにします。

## 見出しと小問を改造

<details><summary>追加したコード</summary>

```test6.typ
#set heading(
  hanging-indent: 1em,
  supplement: none,
  outlined: false,
)
#show heading: set heading(
  numbering: n => box(
    align(center + bottom)[第#h(1em)#n#h(1em)問],
    // stroke: 1pt,
    height: 1em,
    width: 5em,
  ),
)
#show heading: it => {
  set text(
    size: 10pt,
    weight: "regular",
  )
  context counter(heading).display(it.numbering)
  h(1em)
  it.body
}

#let counting-symbols = "1aAiI一壹あいアイא가ㄱ*"
#let consume-regex = regex("[^" + counting-symbols + "]*[" + counting-symbols + "][^" + counting-symbols + "]*")

#show enum.item: it => {
  if it.number == none {
    return it
  }
  // The generated terms is not tight
  // So setting `par.spacing` is to set the result enums' spacing
  let spacing = if enum.spacing != auto {
    enum.spacing
  } else if enum.tight {
    par.leading
  } else {
    par.spacing
  }
  set par(spacing: spacing)


  let new-numbering = if type(enum.numbering) == function or enum.full {
    numbering.with(enum.numbering, it.number)
  } else {
    enum.numbering.trim(consume-regex, at: start, repeat: false)
  }
  let current-number = numbering(enum.numbering, it.number)
  context {
    let hanging-indent = measure(current-number).width + terms
      .separator
      .amount

    set terms(hanging-indent: hanging-indent)

    terms.item(
      strong(delta: -300, numbering(enum.numbering, it.number)),
      {
        if new-numbering != "" {
          set enum(numbering: new-numbering)
          it.body
        } else {
          it.body
        }
      },
    )
  }
}
```
</details>

![test6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3805355/a8a1c6f9-404b-4ee2-a4e7-1a75b67e4bb2.png)

大問のラベルと問題文に一文字分の空白を開けることに成功しましたが、`heading.numbering` と `heading.body` を使って無理やり出力したため、見出し部分が段落判定されています！
この記事を作る前の自分は何が起きているかわかりませんでしたが、記事を書いている中で何となく理解できた気がします。

また小問も `1.` の表記にするとベースラインが合うようになりましたが、 `+` 表記だと高さが変わらないです。
コードを見たところ、無理やり `terms()` にしているみたいで `set enum` の空白設定がうまくいっていないみたいです。


# 終わり

今回はいったんここまでにします。とりあえず段落については多少理解が深まったんじゃないかなと。
目標に向けての課題がたくさんあるため、Typstについてもう少し勉強したいと思います。

3/6追記
正直見出しを無理やり大問にするくらいなら自分で関数を作った方が楽だなと思い、その関数内でインデントを調整することにしました。
`=` の後に書くだけで見出しを表せるというのに憧れていましたが、そこにこだわることができるほど理解していないので...
よく考えたらLaTeXの時もセクションはカウンタだけ借りて、自作の関数で表してたことを思い出しました。