Typstで書く卒論・修論テンプレート
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 表紙
#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が生成されるようにしました.
template.typ
のmaster.thesis
関数内で表紙のレイアウトは直接書いています.
// 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のレイアウトを決めています.
// 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.typ
のmaster_thesis
関数内で直接書いています
// 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.typ
のtoc
関数でレイアウトを決めています
// 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.typ
のtoc_img
とtoc_tbl
でそれぞれ定めています.
// 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
型にして返すような処理をしています.
// 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 図や表の作成
// 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
関数を作って図や表のレイアウトの概要を決めています, 図番号などを引用するときにカウントされるように,
// 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の番号更新を行っています.
// 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 定義, 定理, 補題などの機能
これはこちらを参考に実装しました
// 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.typ
のmaster_thesis
関数内で番号付を定義しています.
// 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.typ
でLATEX
変数を作ってLatex表記ができるようにしました.
// 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に想いを馳せることもできます.
参考
- 今回作成したテンプレ
- 定義, 定理, 補題などの機能を実装するときに参考にしたもの
- Typstの便利なテンプレ集
Discussion