🐥

Typstで書く卒論・修論テンプレート

2023/11/29に公開

1. Typstとは

公式ドキュメントにあるようにマークアップベースの組版システムです. Rust言語で書かれているので, Latexに比べてコンパイルが早いのが特徴です.

2. 修論テンプレート

今年リリースされたこともあってか, 修士論文のテンプレがなかったので自分で作りました.
GitHubからダウンロードできます.

3. 実装したもの

ディレクトリ構成は以下のようになっています.

.
├── Figures
│   ├── typst-github.svg
│   └── typst.svg
├── README.md
├── main.pdf
├── main.typ
├── references.bib
├── template.pdf
└── template.typ

template.typに環境変数やレイアウトを書いていまして, それをmain.typで読み込んで本文を書いていくという感じになっています. また, bibTexを読み込めるので, 参考文献はreferences.bibに記しています.

修論のテンプレの作成においていくつか実装しました.template.typ内で関数や変数を定義しています.

  • thmcounters...
  • thmenv(identifier, base, base_level, fmt)
  • thmbox(identifier, head, ..blockargs, supplement, padding, namefmt, titlefmt, bodyfmt, separator, base, base_level)
  • thmplain ...
  • equation_num(_)
  • table_num(_)
  • image_num(_)
  • tbl(tbl, caption: "")
  • img(img, caption: "")
  • abstract_page(abstract_ja, abstract_en, keywords_ja: (), keywords_en: ())
  • to-string(content)
  • toc()
  • toc_img()
  • toc_tbl()
  • empty_par()
  • master_thesis(title, author, university, school, department, id:, mentor, mentor-post, class, date, paper-type, abstract_ja, abstract_en, keywords_ja, keywords_en, paper-size, bibliography-file, body,)
  • LATEX

3.1 表紙

main.typ
#import "./template.typ": *
#show: master_thesis.with(
  title: "Typstで書く修論のテンプレ",
  author: "右往 左往",
  university: "東京大学大学院",
  school: "工学系研究科",
  department: "航空宇宙工学専攻",
  id: "12-345678",
  mentor: "魚 竿",
  mentor-post: "准教授",
  class: "修士",
  abstract_ja: [
      近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい. 近年の宇宙ってほんますごい.
  ],
  keywords_ja: ("宇宙", "異常検知"),
  bibliography-file: "references.bib",
)

こんな感じでmain.typファイル内でtemplate.typファイルを読み込んでパラメータを設定すると下のような表紙とabstructが生成されるようにしました.
表紙
Abstruct

template.typmaster.thesis関数内で表紙のレイアウトは直接書いています.

template.typ
// The first page.
  align(center)[
    #v(80pt)
    #text(
      size: 16pt,
    )[
      #university #school #department
    ]

    #text(
      size: 16pt,
    )[
      #class#paper-type
    ]
    #v(40pt)
    #text(
      size: 22pt,
    )[
      #title
    ]
    #v(50pt)
    #text(
      size: 16pt,
    )[
      #id #author
    ]

    #text(
      size: 16pt,
    )[
      指導教員: #mentor #mentor-post
    ]
    #v(40pt)
    #text(
      size: 16pt,
    )[
      #date.at(0) 年 #date.at(1) 月 #date.at(2) 日 提出
    ]
    #pagebreak()
  ]

  set page(
    footer: [
      #align(center)[#counter(page).display("i")]
    ]
  )

  counter(page).update(1)
  // Show abstruct
  abstract_page(abstract_ja, abstract_en, keywords_ja: keywords_ja, keywords_en: keywords_en)

最後の行でabstract_page関数を呼び出していまして, この関数内でAbstructのレイアウトを決めています.

template.typ
// Definition of abstruct page
#let abstract_page(abstract_ja, abstract_en, keywords_ja: (), keywords_en: ()) = {
  if abstract_ja != [] {
    show <_ja_abstract_>: {
      align(center)[
        #text(size: 20pt, "概要")
      ]
    }
    [= 概要 <_ja_abstract_>]

    v(30pt)
    set text(size: 12pt)
    h(1em)
    abstract_ja
    par(first-line-indent: 0em)[
      #text(weight: "bold", size: 12pt)[
      キーワード:
      #keywords_ja.join(", ")
      ]
    ]
  } else {
    show <_en_abstract_>: {
      align(center)[
        #text(size: 18pt, "Abstruct")
      ]
    }
    [= Abstract <_en_abstract_>]

    set text(size: 12pt)
    h(1em)
    abstract_en
    par(first-line-indent: 0em)[
      #text(weight: "bold", size: 12pt)[
        Key Words: 
        #keywords_en.join("; ")
      ]
    ]
  }
}

3.2 セクション分け


章, 節, 項とこちらのレイアウトにしました.
template.typmaster_thesis関数内で直接書いています

template.typ
// Configure paragraph properties.
  set par(leading: 0.78em, first-line-indent: 12pt, justify: true)
  show par: set block(spacing: 0.78em)

   // Configure chapter headings.
  set heading(numbering: (..nums) => {
    nums.pos().map(str).join(".") + " "
  })
  show heading.where(level: 1): it => {
    pagebreak()
    counter(math.equation).update(0)
    set text(weight: "bold", size: 20pt)
    set block(spacing: 1.5em)
    let pre_chapt = if it.numbering != none {
          text()[
            #v(50pt)
            第
            #numbering(it.numbering, ..counter(heading).at(it.location()))
            章
          ] 
        } else {none}
    text()[
      #pre_chapt \
      #it.body \
      #v(50pt)
    ]
  }
  show heading.where(level: 2): it => {
    set text(weight: "bold", size: 16pt)
    set block(above: 1.5em, below: 1.5em)
    it
  }

  show heading: it => {
    set text(weight: "bold", size: 14pt)
    set block(above: 1.5em, below: 1.5em)
    it
  } + empty_par()

レイアウトは章の始まりで改ページするようにしていまして, 大きく表示するようにしています. headingがSectionを表していまして, level: 1が章, level: 2が節みたいな感じ(TexでいうSection, Subsection的なもの)で, 指定することができます.

3.3 目次


章, 節, 項と階段状になるようなレイアウトにしました.

template.typtoc関数でレイアウトを決めています

template.typ
// Definition of chapter outline
#let toc() = {
  align(left)[
    #text(size: 20pt, weight: "bold")[
      #v(30pt)
      目次
      #v(30pt)
    ]
  ]

  set text(size: 12pt)
  set par(leading: 1.24em, first-line-indent: 0pt)
  locate(loc => {
    let elements = query(heading.where(outlined: true), loc)
    for el in elements {
      let before_toc = query(heading.where(outlined: true).before(loc), loc).find((one) => {one.body == el.body}) != none
      let page_num = if before_toc {
        numbering("i", counter(page).at(el.location()).first())
      } else {
        counter(page).at(el.location()).first()
      }

      link(el.location())[#{
        // acknoledgement has no numbering
        let chapt_num = if el.numbering != none {
          numbering(el.numbering, ..counter(heading).at(el.location()))
        } else {none}

        if el.level == 1 {
          set text(weight: "black")
          if chapt_num == none {} else {
            chapt_num
            "  "
          }
          let rebody = to-string(el.body)
          rebody
        } else if el.level == 2 {
          h(2em)
          chapt_num
          " "
          let rebody = to-string(el.body)
          rebody
        } else {
          h(5em)
          chapt_num
          " "
          let rebody = to-string(el.body)
          rebody
        }
      }]
      box(width: 1fr, h(0.5em) + box(width: 1fr, repeat[.]) + h(0.5em))
      [#page_num]
      linebreak()
    }
  })
}

また表目次,図目次も同様に決めていまして, template.typtoc_imgtoc_tblでそれぞれ定めています.

template.typ
// Definition of image outline
#let toc_img() = {
  align(left)[
    #text(size: 20pt, weight: "bold")[
      #v(30pt)
      図目次
      #v(30pt)
    ]
  ]

  set text(size: 12pt)
  set par(leading: 1.24em, first-line-indent: 0pt)
  locate(loc => {
    let elements = query(figure.where(outlined: true, kind: "image"), loc)
    for el in elements {
      let chapt = counter(heading).at(el.location()).at(0)
      let num = counter(el.kind + "-chapter" + str(chapt)).at(el.location()).at(0) + 1
      let page_num = counter(page).at(el.location()).first()
      let caption_body = to-string(el.caption.body)
      str(chapt)
      "."
      str(num)
      h(1em)
      caption_body
      box(width: 1fr, h(0.5em) + box(width: 1fr, repeat[.]) + h(0.5em))
      [#page_num]
      linebreak()
    }
  })
}

// Definition of table outline
#let toc_tbl() = {
  align(left)[
    #text(size: 20pt, weight: "bold")[
      #v(30pt)
      表目次
      #v(30pt)
    ]
  ]

  set text(size: 12pt)
  set par(leading: 1.24em, first-line-indent: 0pt)
   locate(loc => {
    let elements = query(figure.where(outlined: true, kind: "table"), loc)
    for el in elements {
      let chapt = counter(heading).at(el.location()).at(0)
      let num = counter(el.kind + "-chapter" + str(chapt)).at(el.location()).at(0) + 1
      let page_num = counter(page).at(el.location()).first()
      let caption_body = to-string(el.caption.body)
      str(chapt)
      "."
      str(num)
      h(1em)
      caption_body
      box(width: 1fr, h(0.5em) + box(width: 1fr, repeat[.]) + h(0.5em))
      [#page_num]
      linebreak()
    }
  })
}

ここでto-stringという関数を定義していますが, 図や表のcaptionに文献を引用している場合,(図目次, 表目次のページが本文より先にくるため)文献の引用番号が優先的に振られてしまう事象が起きてしまったため, 文献の引用番号を省く工夫をしています.

詳しく言うと, 図や表のCaptionはcontent型となっていて, 引用などで使われる@から始まる文字列と普通の文章などが別れて格納されている(便利)ので, 文章だけを取り出してstring型にして返すような処理をしています.

template
// Definition of content to string
#let to-string(content) = {
  if content.has("text") {
    content.text
  } else if content.has("children") {
    content.children.map(to-string).join("")
  } else if content.has("body") {
    to-string(content.body)
  } else if content == [ ] {
    " "
  }
}

3.5 図や表の作成

template.typ
// Definition of table format
#let tbl(tbl, caption: "") = {
  figure(
    tbl,
    caption: caption,
    supplement: [表],
    numbering: table_num,
    kind: "table",
  )
}

// Definition of image format
#let img(img, caption: "") = {
  figure(
    img,
    caption: caption,
    supplement: [図],
    numbering: image_num,
    kind: "image",
  )
}

tbl関数やimg関数を作って図や表のレイアウトの概要を決めています, 図番号などを引用するときにカウントされるように,

template.typ
// Counting equation number
#let equation_num(_) = {
  locate(loc => {
    let chapt = counter(heading).at(loc).at(0)
    let c = counter(math.equation)
    let n = c.at(loc).at(0)
    "(" + str(chapt) + "." + str(n) + ")"
  })
}

// Counting table number
#let table_num(_) = {
  locate(loc => {
    let chapt = counter(heading).at(loc).at(0)
    let c = counter("table-chapter" + str(chapt))
    let n = c.at(loc).at(0)
    str(chapt) + "." + str(n + 1)
  })
}

図や表を作成する毎に, "table-chapter""image-chapter"がカウントされるようにしています. 'master_thesis'関数内でCaptionの番号更新を行っています.

template.typ
// counting caption number
  show figure: it => {
    set align(center)
    if it.kind == "image" {
      set text(size: 12pt)
      it.body
      it.supplement
      " " + it.counter.display(it.numbering)
      " " + it.caption.body
      locate(loc => {
        let chapt = counter(heading).at(loc).at(0)
        let c = counter("image-chapter" + str(chapt))
        c.step()
      })
    } else if it.kind == "table" {
      set text(size: 12pt)
      it.supplement
      " " + it.counter.display(it.numbering)
      " " + it.caption.body
      set text(size: 10.5pt)
      it.body
      locate(loc => {
        let chapt = counter(heading).at(loc).at(0)
        let c = counter("table-chapter" + str(chapt))
        c.step()
      })
    } else {
      it
    }
  }

3.6 定義, 定理, 補題などの機能


これはこちらを参考に実装しました

template.typ
// Store theorem environment numbering
#let thmcounters = state("thm",
  (
    "counters": ("heading": ()),
    "latest": ()
  )
)

// Setting theorem environment
#let thmenv(identifier, base, base_level, fmt) = {

  let global_numbering = numbering

  return (
    ..args,
    body,
    number: auto,
    numbering: "1.1",
    refnumbering: auto,
    supplement: identifier,
    base: base,
    base_level: base_level
  ) => {
    let name = none
    if args != none and args.pos().len() > 0 {
      name = args.pos().first()
    }
    if refnumbering == auto {
      refnumbering = numbering
    }
    let result = none
    if number == auto and numbering == none {
      number = none
    }
    if number == auto and numbering != none {
      result = locate(loc => {
        return thmcounters.update(thmpair => {
          let counters = thmpair.at("counters")
          // Manually update heading counter
          counters.at("heading") = counter(heading).at(loc)
          if not identifier in counters.keys() {
            counters.insert(identifier, (0, ))
          }

          let tc = counters.at(identifier)
          if base != none {
            let bc = counters.at(base)

            // Pad or chop the base count
            if base_level != none {
              if bc.len() < base_level {
                bc = bc + (0,) * (base_level - bc.len())
              } else if bc.len() > base_level{
                bc = bc.slice(0, base_level)
              }
            }

            // Reset counter if the base counter has updated
            if tc.slice(0, -1) == bc {
              counters.at(identifier) = (..bc, tc.last() + 1)
            } else {
              counters.at(identifier) = (..bc, 1)
            }
          } else {
            // If we have no base counter, just count one level
            counters.at(identifier) = (tc.last() + 1,)
            let latest = counters.at(identifier)
          }

          let latest = counters.at(identifier)
          return (
            "counters": counters,
            "latest": latest
          )
        })
      })

      number = thmcounters.display(x => {
        return global_numbering(numbering, ..x.at("latest"))
      })
    }

    return figure(
      result +  // hacky!
      fmt(name, number, body, ..args.named()) +
      [#metadata(identifier) <meta:thmenvcounter>],
      kind: "thmenv",
      outlined: false,
      caption: none,
      supplement: supplement,
      numbering: refnumbering,
    )
  }
}

// Definition of theorem box
#let thmbox(
  identifier,
  head,
  ..blockargs,
  supplement: auto,
  padding: (top: 0.5em, bottom: 0.5em),
  namefmt: x => [(#x)],
  titlefmt: strong,
  bodyfmt: x => x,
  separator: [#h(0.1em):#h(0.2em)],
  base: "heading",
  base_level: none,
) = {
  if supplement == auto {
    supplement = head
  }
  let boxfmt(name, number, body, title: auto) = {
    if not name == none {
      name = [ #namefmt(name)]
    } else {
      name = []
    }
    if title == auto {
      title = head
    }
    if not number == none {
      title += " " + number
    }
    title = titlefmt(title)
    body = bodyfmt(body)
    pad(
      ..padding,
      block(
        width: 100%,
        inset: 1.2em,
        radius: 0.3em,
        breakable: false,
        ..blockargs.named(),
        [#title#name#separator#body]
      )
    )
  }
  return thmenv(
    identifier,
    base,
    base_level,
    boxfmt
  ).with(
    supplement: supplement,
  )
}

// Setting plain version
#let thmplain = thmbox.with(
  padding: (top: 0em, bottom: 0em),
  breakable: true,
  inset: (top: 0em, left: 1.2em, right: 1.2em),
  namefmt: name => emph([(#name)]),
  titlefmt: emph,
 )

3.9 番号のカウント

論文(工学系や一部理学系)では, 「図1.2」, 「式(3.3)」など, {章番号}+{連番}の番号付けがされることが多いです. なのでそのような実装をするために, template.typmaster_thesis関数内で番号付を定義しています.

template.typ
// citation number
  show ref: it => {
    if it.element != none and it.element.func() == figure {
      let el = it.element
      let loc = el.location()
      let chapt = counter(heading).at(loc).at(0)

      link(loc)[#if el.kind == "image" or el.kind == "table" {
          // counting 
          let num = counter(el.kind + "-chapter" + str(chapt)).at(loc).at(0) + 1
          it.element.supplement
          " "
          str(chapt)
          "."
          str(num)
        } else if el.kind == "thmenv" {
          let thms = query(selector(<meta:thmenvcounter>).after(loc), loc)
          let number = thmcounters.at(thms.first().location()).at("latest")
          it.element.supplement
          " "
          numbering(it.element.numbering, ..number)
        } else {
          it
        }
      ]
    } else if it.element != none and it.element.func() == math.equation {
      let el = it.element
      let loc = el.location()
      let chapt = counter(heading).at(loc).at(0)
      let num = counter(math.equation).at(loc).at(0)

      it.element.supplement
      " ("
      str(chapt)
      "."
      str(num)
      ")"
    } else if it.element != none and it.element.func() == heading {
      let el = it.element
      let loc = el.location()
      let num = numbering(el.numbering, ..counter(heading).at(loc))
      if el.level == 1 {
        str(num)
        "章"
      } else if el.level == 2 {
        str(num)
        "節"
      } else if el.level == 3 {
        str(num)
        "項"
      }
    } else {
      it
    }
  }

3.8 Latexへのリスペクトも忘れずに

template.typLATEX変数を作ってLatex表記ができるようにしました.

template.typ
// LATEX character
#let LATEX = {
  [L];box(move(
    dx: -4.2pt, dy: -1.2pt,
    box(scale(65%)[A])
  ));box(move(
  dx: -5.7pt, dy: 0pt,
  [T]
));box(move(
  dx: -7.0pt, dy: 2.7pt,
  box(scale(100%)[E])
));box(move(
  dx: -8.0pt, dy: 0pt,
  [X]
));h(-8.0pt)
}

LATEX
これでLatexに想いを馳せることもできます.

参考

  • 今回作成したテンプレ
  • 定義, 定理, 補題などの機能を実装するときに参考にしたもの
  • Typstの便利なテンプレ集

Discussion