🗂

Typstの段組み徹底攻略:綺麗な2段組を目指せ

2024/12/24に公開

Typstとは

Typst はアカデミック・ライティング用途を念頭に開発された、TeXなどの今までのシステムを覆しうる革新的で多機能な組版エンジンです。もちろん、学術論文に限らず、雑誌や書籍の組版にも適しています。

https://typst-jp.github.io/docs/

段組とは

段組(だんぐみ、multi-column format)「連続する1系列の文章を1ページの中で,字詰め方向において,2つ以上の部分(段)に分割し,各部分の間には空白(段間)を設けて文字を配置する方法.(JIS Z 8125)」

https://www.w3.org/TR/2012/NOTE-jlreq-20120403/ja/#term.multi-column-format

n段組は、簡単に言えば、同じページの文章が二つの段(コラム)に分けられることです。つまり、一つのページを縦で割って、複数のちっちゃなページを同じページに置きます。文章は、同じページにおいて、まず左のコラム最初から最後まで流れ、そのあとに次のコラムに進み、このページのすべてのコラムが全部流れきったところで次のページに行くことになります。

よく見る段組は、一段組と二段組(1段組みや2段組みのようにも書く)ですが、ページのサイズやレイアウト、メディアの種類によっては、3段組やそれ以上のn段組もあります。また、横書きだけではなく、縦書きでもn段書きがあります。横書きの二段組は特に理系の論文でよく使われる組版形式の一つで、縦組のn段組は雑誌や新聞でよく見られます。

Typstで段組する方法

一段組

一段組は即ち段組みが行われていない状態です。Typstでも何もしなければ一段組になります。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set par(first-line-indent: 1em)

夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

二段組

二段組にすること自体は簡単です。#set page(columns: 2)を追加することでできます。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set par(first-line-indent: 1em)
#set page(columns: 2) // **ここ注目**

夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

左右を揃える

前章で、二段組みすることができたのですが、おわかりいただけただろうか😨……左右が揃っていません!!!😱

理由を探ってみましょう。よく見ると、段落と段落との間と、段落内の行と行の間の距離が違います。日本語の組版では、字下げをすることによって段落を区別しているのが普通で、段落間と行間の距離を特に空けません。

4.5.2 段落間処理
段落と段落の間は,特に指定がない場合は,その段落で指定されている行間を確保すればよい.
注1)
JIS X 4051では,“異なる文字サイズや行間の段落が連続する場合は,基本版面の行間とする.”と規定している.

https://www.w3.org/TR/2012/NOTE-jlreq-20120403/ja/#processing_of_spaces_between_paragraphs

解決法は簡単です。行間距離と段間距離を同じにすればいいです。行間距離を制御するのはpar.leadingで、段間距離を制御するのはpar.spacingです。この二つを同じ値にすることで揃えることができます。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set par(
  first-line-indent: 1em,
  leading: 1em, // **ここ注目** 行間距離
  spacing: 1em, // **ここ注目** 段間距離
)


夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

任意段組み

三段組なら#set page(columns: 3)、四段組なら#set page(columns: 4)でできます。簡単でしょ?

段組の段間距離を調整する

段間(だんかん、column gap)「段組の段と段との間の空き.」(JIS Z 8125)

https://www.w3.org/TR/2012/NOTE-jlreq-20120403/ja/#term.column-gap

段と段(コラムとコラム)の間の距離は、段間と言います。段間を調整するにはpageではなく、columnsを調整します。#set columns(gutter: 15em)などを追加し、2emを好きな距離に置き換えばできます。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set columns(gutter: 15em) // **ここ注目**
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)


夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

部分的段組み

全体は一段組で、一部分だけ段組みしたい場合は、以下のようにcolumns()関数を使います。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set columns(gutter: 2em)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)


夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

// **ここ注目**
#columns(
  4,
  gutter: 0.5em,
)[
  #roremu(180, offset: 8)
  #roremu(180, offset: 8)
]

また、箱(boxblock)に抑え込むことで1ページに複数の段組を入れることができます。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set columns(gutter: 2em)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)


夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#block(
  fill: gradient.linear(rgb(240, 255, 255), rgb(255, 255, 240)),
  height: 10em,
  radius: 8pt,
  stroke: 1pt + black,
  inset: (x: 10pt, y: 10pt),
)[
  #columns(
    4,
    gutter: 0.5em,
  )[
    #roremu(97, offset: 8)
  ]
]


#block(
  fill: gradient.linear(rgb(255, 240, 255), rgb(240, 255, 240)),
  stroke: 1pt + black,
  height: 10em,
  radius: 8pt,
  inset: (x: 10pt, y: 10pt),
)[
  #columns(
    3,
    gutter: 0.5em,
  )[
    #roremu(97, offset: 8)
  ]
]

段組のバランス調節

時々に文章の長さによっては右が殆どないのに、左の方が非常に長いときってありますよね。そういうとこに全体が短くてもいいからいいところで切って欲しいですよね。そういう時に持ってこいのはcolbreak()関数です。

失敗例のコード
#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)

夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)

夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(155, offset: 8)
#colbreak()
#roremu(180, offset: 177)

段組から抜け出す

時に図表を挿入する場合など、段組から逃げたいときがありますよね。例えば以下のような小さいグラフならいざ知らず、大きなグラフを挿入したい時は、それだけ段組みから外したい(つまりそこだけ一段組すること)です。

コラム内の図表のコード
#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)


夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, off![](https://storage.googleapis.com/zenn-user-upload/149696b73f68-20241223.png)set: 8)

#roremu(180, offset: 8)

#figure(
  box(fill: gradient.linear(red, blue), width: 1fr, radius: 8pt, inset: (x: 10pt, y: 10pt)),
  caption: "謎のグラデ",
)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

今までは簡単にできなかったのですが、最近になってようやくことの機能が提供されました。

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)
#set page(columns: 2)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)

夏目漱石「吾輩は猫である」

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#place(
  bottom + left,
  scope: "parent",
  float: true,
  figure(
    box(fill: gradient.linear(red, blue), width: 1fr, radius: 8pt, inset: (x: 10pt, y: 10pt)),
    caption: "謎のグラデ",
  ),
)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

真ん中に図表を刺す

流石に真ん中に図表を刺すことまでは公式の機能として提供されていないので、簡単にはできないが、最初から全ページのpage.columnsを一旦取り消して、図表前の内容、図表、図表後の内容の3つの部分に分けて、図表の前後にそれぞれcolumns()を消すことでできますが、colbreakで調整したりしないといけないので手間がかかります。

段組の間に縦線を刺す

まだ公式の方法はないのですが、一応2024年11月時点でIssueが建てられています。
https://github.com/typst/typst/issues/5448

今できる解決法は、手動で線を書く必要があります。一例として、全部が段組の場合のやり方を挙げます。

二段組ページのど真ん中に刺す

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)

#let get-page-margin-top-and-bottom() = {
  let smaller-side = calc.min(page.width.pt(), page.height.pt())
  let auto-margin = 2.5 / 21 * smaller-side

  if page.margin == auto {
    (auto-margin, auto-margin)
  } else if type(page.margin) == length {
    (page.margin, page.margin)
  } else {
    let top-margin = page.margin.at(
      "top",
      default: page.margin.at("y", default: margin.at("rest", default: auto-margin)),
    )
    let bottom-margin = page.margin.at(
      "bottom",
      default: page.margin.at("y", default: margin.at("rest", default: auto-margin)),
    )
    (top-margin, bottom-margin)
  }
}

#set page(
  columns: 2,
  foreground: context {
    let (top-margin, bottom-margin) = get-page-margin-top-and-bottom()

    let (extend-top, extend-bottom) = (5pt, 5pt)

    place(
      top + left,
      line(
        start: (50%, top-margin * 1pt - extend-top
        ),
        end: (50%, page.height - (bottom-margin * 1pt) + extend-bottom),
      ),
    )
  },
)
#set columns(gutter: 2em)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)

#show columns: it => {
  it
}

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

任意の段落に自動的に適用

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP", 18pt)

/// Calculate evaluated page margin as a list of four `length` values.
/// -> list
#let get-computed-page-margin() = {
  let smaller-side = calc.min(page.width, page.height)
  let auto-margin = 2.5 / 21 * smaller-side

  if page.margin == auto {
    (auto-margin, auto-margin, auto-margin, auto-margin)
  } else if type(page.margin) == length {
    (page.margin, page.margin, page.margin, page.margin)
  } else {
    let top-margin = page.margin.at(
      "top",
      default: page.margin.at("y", default: margin.at("rest", default: auto-margin)),
    )
    let bottom-margin = page.margin.at(
      "bottom",
      default: page.margin.at("y", default: margin.at("rest", default: auto-margin)),
    )
    (top-margin, bottom-margin, top-margin, bottom-margin)
  }
}

#set page(
  columns: 3,
  foreground: context {
    let (top-margin, bottom-margin, left-margin, right-margin) = get-computed-page-margin()

    let (extend-top, extend-bottom) = (5pt, 5pt)

    let content-width = page.width - (left-margin + right-margin)
    let column-width = (content-width - (columns.gutter * (page.columns - 1))) / page.columns

    let separator-coordinates = range(1, page.columns).map(i => {
      let separator-relative-position = (i * column-width) + ((i - 0.5) * columns.gutter)
      (
        left-margin + separator-relative-position,
        top-margin - extend-top,
        left-margin + separator-relative-position,
        page.height - (bottom-margin) + extend-bottom,
      )
    })

    for (x1, y1, x2, y2) in separator-coordinates {
      place(
        top + left,
        line(
          start: (x1, y1),
          end: (x2, y2),
          stroke: red,
        ),
      )
    }
  },
)

#set columns(gutter: 2em)
#set par(
  first-line-indent: 1em,
  leading: 1em,
  spacing: 1em,
)

#roremu(8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

#roremu(180, offset: 8)

page.columnsを4にしたら、こうなります。

対訳テキスト風並行二段組

普通の二段組は1ページ目の左→1ページ目の右→2ページ目の左→2ページ目の右→……のように、一つのテキストが複数ページがあるように右へ流れていくわけですが、二つのテキストを左右で並べて、それぞれ半分を占め、左のテキストは1ページ目の左→2ページ目の左→……、右のテキストは1ページ目の右→2ページ目の右→……のように、別々に流れていくわけですが、以下を参考に、このようにすれば左右の対訳ができ、さらに同じセルがページを跨る(fragmentation)こともできます。

https://zenn.dev/hidaruma/articles/fd70abcec61ac1

https://forum.typst.app/t/how-to-typeset-two-texts-in-parallel-on-pairs-of-facing-pages/1314

#import "@preview/roremu:0.1.0": roremu

#set text(lang: "ja", font: "Noto Sans CJK JP")
#set columns(gutter: 1em)
#set par(justify: true)

#let l-style = it => {
  set text(lang: "la", font: "Roboto")
  it
}

#let r-style = it => {
  set text(lang: "ja", font: "Noto Sans CJK JP")
  it
}

#let lr-blocks = state("lr-blocks", 0)
#let block-number() = context text(0.8em, numbering("1", lr-blocks.get()))

#let lr-columns(a, b) = {
  lr-blocks.update(x => x + 1)
  grid(
    columns: (1fr, 1fr),
    column-gutter: 2em,
    place(left, dx: -1.5em, block-number()) + l-style(a), place(right, dx: 1.5em, block-number()) + r-style(b),
  )
}

#lr-columns[ #lorem(10) ][ #roremu(16) ]
#lr-columns[ #lorem(35) ][ #roremu(69) ]
#lr-columns[ #lorem(10) ][ #roremu(33) ]
#lr-columns[ #lorem(35) ][ #roremu(69) ]
#lr-columns[ #lorem(10) ][ #roremu(33) ]
#lr-columns[ #lorem(35) ][ #roremu(69) ]
#lr-columns[ #lorem(10) ][ #roremu(33) ]
#lr-columns[ #lorem(10) ][ #roremu(16) ]
#lr-columns[ #lorem(10) ][ #roremu(33) ]
#lr-columns[ #lorem(35) ][ #roremu(69) ]
#lr-columns[ #lorem(10) ][ #roremu(16) ]
#lr-columns[ #lorem(10) ][ #roremu(16) ]
#lr-columns[ #lorem(43) ][ #roremu(154) ]
#lr-columns[ #lorem(10) ][ #roremu(16) ]


募集

もし他にアイデア、もっといいやり方、解決したい問題などがあればぜひコメントなどからご連絡下さい。

コミュニティ

「くみはんクラブ」というコミュニティを2024年1月に創設し、そこでTypstを始めとする様々な組版の話について議論されています。何かTypstなどを使う上で質問や不明点があれば、ぜひいらしてください。また、そのメンバーが中心となって、Typst Japan Communityを結成し、公式の認可を得てドキュメントの非公式日本語翻訳プロジェクトも始動しました。

https://discord.gg/dHRaAsBeRY

https://x.com/mkpoli/status/1751193383085973670

https://x.com/mkpoli/status/1827242838880408036

https://x.com/mkpoli/status/1858311758517326002

Discussion