はじめに
これまで文書作成にはLaTeXを愛用していました。しかし、大学生になりレポートを書く機会が増え、箇条書きのたびに記述しないといけない呪文(\{enumerate})や、度重なるバックスラッシュの嵐、エラーメッセージの不親切さに少々疲弊していました。
でも絶っっ対にMicrosoft Wordとかいうエディタだけは使いたくない自分がいるので、 他にいい組版ソフトがないかなーと探していたところ、
コンパイルをリアルタイムで行ってくれて、Markdown感覚で記述できるものがあるよ
というGemini。リアルタイムのコンパイル!?すごすぎないか。これが僕とTypstとの出会いでした。
Typstの素晴らしさについては他の方が解説していらっしゃると思うのでここらでさておいて、、、
今回は、Typstの自作のライブラリと自作コマンドを作って、大学のレポート作成を快適化した話です。お付き合いいただけると嬉しいです。
解決したかった2つの課題
確かにTypstは便利ですが、そのままでは組版ソフトならではの課題が残ります。その中でも私が特に解決したかったのが、
- ページや余白の設定を毎回記述するのが大変
- 毎回ページの余白は20mmで、2段組で、、なんて設定するのが大変
- 設定項目に肝心の本文が埋もれてしまうのも嫌
- 参考文献のリンクが面倒すぎる
- 相互参照するたびに
#link(<ref1>)[#super[[1]]なんて長々とタイピングしたくない
- 相互参照するたびに
という点です。理想はtouch report.typしたら、タイトルや副題、名前を入力してすぐに本文!と書き始められるのが良いのですが、実際はタイトルの文字サイズを20pt、フォントはHagino Aji Gothic、なんか余白きもいな、、とりあえず#v(-1em)で揃えるか、、あれ、2段組ってどうするんだっけ?のように執筆意欲を下げる過程が続々と登場します。
レポートの型はだいたい同じなので、テンプレートを作ってしまおう!という発想に至るまでの時間はそう短くありませんでした。
成果物
今回私が製作したものは、こちらです。ローカルにTypstの自作ライブラリを作り、コマンドラインからmkrep 'レポートのタイトル'を呼び出すと、自動でそのライブラリをimportしてくれたTypstのファイルを生成してVSCodeで起動してくれる、というものを作りました。
[package]
name = "my-report"
version = "0.1.0"
entrypoint = "lib.typ"
#let report-template(
title: "",
subtitle: none,
author: "",
student-id: none,
date: none,
subject: none,
show-cover: true,
show-page-num: true,
cols: 1,
body
) = {
set page(
paper: "a4",
margin: (top: 30mm, bottom: 30mm, left: 22mm, right: 22mm),
header: context {
if counter(page).get().first() > 1 {
align(right, text(size: 8pt, fill: rgb("#606060"), font: "Harano Aji Gothic")[
#show linebreak: ""
#title
])
}
},
footer: context {
let raw-page = counter(page).get().first()
if show-page-num {
if show-cover {
if raw-page > 1 {
let logical-page = raw-page - 1
align(center, text(size: 10pt)[\- #logical-page \-])
}
} else {
align(center, text(size: 10pt)[\- #raw-page \-])
}
}
}
)
set heading(numbering: (..args) => {
let nums = args.pos()
if nums.len() == 1 {
[#nums.at(0). ]
} else if nums.len() == 2 {
[#nums.at(0).#nums.at(1). ]
} else if nums.len() == 3 {
[#nums.at(0).#nums.at(1).#nums.at(2). ]
}
})
show heading.where(level: 1): it => {
set text(size: 12pt, weight: "bold", font: "Harano Aji Gothic")
block(above: 2.5em, below: 0em, it)
}
show heading.where(level: 2): it => {
set text(size: 10.5pt, weight: "bold", font: "Harano Aji Gothic")
block(above: 2.0em, below: 0em, it)
}
show heading.where(level: 3): it => {
set text(size: 10.5pt, weight: "medium", font: "Harano Aji Gothic")
block(above: 1.5em, below: 0em, it)
}
show heading: it => it + par(text(size: 0pt, ""))
show footnote.entry: set par(leading: 1em, first-line-indent: 1em)
show footnote.entry: set text(size: 8.5pt, font: "Harano Aji Mincho")
// --- 1. 本文中の @1 や @2 などの参照リンクを [1] の上付きに変換 ---
show ref: it => {
let label-str = str(it.target)
if label-str.match(regex("^\d+$")) != none {
super(link(it.target)[[#label-str]])
} else {
it
}
}
set text(
font: ("Harano Aji Mincho", "Harano Aji Gothic"),
size: 10.5pt,
lang: "ja",
)
set par(
justify: true,
leading: 1.0em,
first-line-indent: 1em,
)
set footnote.entry(
gap: 1em,
)
if show-cover {
align(center + horizon)[
#par(leading: 2.0em, first-line-indent: 0em)[
#text(size: 22pt, weight: "bold", font: "Harano Aji Gothic")[#title] \
]
#if subtitle != none {
v(3em)
par(first-line-indent: 0em)[
#text(size: 14pt, weight: "medium", font: "Harano Aji Mincho")[#subtitle]
]
}
#v(3em)
]
align(right + bottom)[
#block(width: 36%, align(left)[
#set text(size: 12pt, font: "Harano Aji Gothic")
#grid(
columns: (50pt, 15pt, 1fr),
row-gutter: 1.2em,
if student-id != none [学籍番号], if student-id != none [:], if student-id != none [#student-id],
if author != none [氏名], if author != none [:], if author != none [#author],
if date != none [提出日], if date != none [:], if date != none [#date],
)
])
]
pagebreak()
}
if not show-cover {
align(center + horizon)[
#v(-3em)
#par(leading: 1.5em, first-line-indent: 0em)[
#text(size: 18pt, weight: "bold", font: "Harano Aji Gothic")[#title] \
]
#if subtitle != none {
v(1em)
par(first-line-indent: 0em)[
#text(size: 12pt, weight: "medium", font: "Harano Aji Mincho")[#subtitle]
]
}
#v(0.7em)
]
align(right)[
#text(size: 10pt, font: "Harano Aji Gothic")[
#if author != none [#author] #if student-id != none [ ( #student-id )] #if date != none [ | #date]
]
]
v(3em)
}
if cols > 1 {
show: columns.with(cols, gutter: 15pt)
body
} else {
body
}
}
// 参考文献用の関数
#let references(title: "参考文献", body) = {
v(2.5em)
pad(left: 0.5em)[
#heading(level: 1, numbering: none, outlined: false)[#title]
]
show link: lk => {
set text(size: 7pt, font: "Harano Aji Gothic")
[\ #text(lk)]
}
show list: lt => {
set text(size: 8pt, font: "Harano Aji Mincho")
set par(leading: 0.8em, first-line-indent: 0em)
v(1em)
let items = lt.children
for (i, item) in items.enumerate() {
let num = i + 1
let label-obj = label(str(num)) // 動的に <1>, <2> などのラベルを生成
block(width: 100%, below: 0.8em)[
#grid(
columns: (22pt, 1fr),
row-gutter: 1em,
align(right)[[#num]\ ],
pad(left: 0.5em)[#item.body #label-obj]
)
]
v(1em)
}
}
body
}
mkrep() {
if [ -z "$1" ]; then
echo "Error: Enter report's title."
return 1
fi
local title="$1"
local filename="${title}.typ"
# すでに同名のファイルが存在する場合は上書き防止のために停止
if [ -f "$filename" ]; then
echo "Error: '$filename' is already exist."
return 1
fi
# 現在の日付を取得 (例: 2026年7月14日)
local current_date=$(date "+%Y年 %m月 %d日")
cat << EOF > "$filename"
#import "@local/my-report:0.1.0": report-template, references
#show: report-template.with(
title: [${title}],
subtitle: none,
author: "XXXX XXXX",
student-id: "00000000",
date: "${current_date}",
subject: none,
show-cover: true,
show-page-num: true,
cols: 2,
)
=
#references(title: "参考文献")[
-
]
EOF
echo "Success: '${filename}' has been created."
if command -v code >/dev/null 2>&1; then
code "$filename"
fi
}
これでターミナルからmkrep 'rep_test'を実行するとカレントディレクトリ内にrep_test.typを自動で生成し、VSCodeで開いてくれるようになりました。
ここまでみてくださってありがとうございます!今日は疲れたのでこの辺で。
開発の際のドラマは、随時更新します。