Commit d7979b2
Changed files (5)
pages/bachelor-outline-page-en.typ
@@ -0,0 +1,97 @@
+#import "../utils/invisible-heading.typ": invisible-heading
+#import "../utils/style.typ": 字号, 字体
+#import "@preview/numbly:0.1.0": numbly
+
+// 本科生英文目录
+#let bachelor-outline-page-en(
+ // documentclass 传入参数
+ twoside: false,
+ fonts: (:),
+ // 其他参数
+ depth: 4,
+ title: "Contents",
+ entry-numbering: numbly("Chapter {1}", "{1}.{2}"),
+ outlined: false,
+ title-vspace: 14pt,
+ title-text-args: auto,
+ // 引用页码字体与字号
+ reference-font: auto,
+ reference-size: 字号.小四,
+ // 字体与字号
+ font: auto,
+ size: (字号.四号, 字号.小四),
+ weight: ("bold", "bold", "regular"),
+ // 目录样式
+ above: (20pt, 14pt),
+ below: (14pt, 14pt),
+ indent: (0pt, 18pt, 28pt),
+ fill: (repeat([.], gap: 0.15em),),
+ gap: .3em,
+) = {
+ // 1. 默认参数
+ fonts = 字体 + fonts
+ if title-text-args == auto {
+ title-text-args = (font: fonts.宋体, size: 字号.小三, weight: "bold")
+ }
+ if reference-font == auto {
+ reference-font = fonts.宋体
+ }
+ if font == auto {
+ font = (fonts.宋体, fonts.宋体, fonts.宋体)
+ }
+
+ // 2. 正式渲染
+ pagebreak(weak: true, to: if twoside { "odd" })
+
+ // 默认显示的字体
+ set text(font: reference-font, size: reference-size)
+
+ {
+ set align(center)
+ text(..title-text-args, title)
+ // 标记一个不可见的标题用于目录生成
+ invisible-heading(level: 1, outlined: outlined, title)
+ }
+
+ v(title-vspace)
+
+ // 目录样式
+ set outline(indent: level => indent.slice(0, calc.min(level + 1, indent.len())).sum())
+ show outline.entry: entry => block(
+ above: above.at(entry.level - 1, default: above.last()),
+ below: below.at(entry.level - 1, default: below.last()),
+ link(
+ entry.element.location(),
+ entry.indented(
+ none,
+ {
+ text(
+ font: font.at(entry.level - 1, default: font.last()),
+ size: size.at(entry.level - 1, default: size.last()),
+ weight: weight.at(entry.level - 1, default: weight.last()),
+ {
+ if entry.element.numbering != none {
+ numbering(entry-numbering, ..counter(outline.target).at(entry.element.location()))
+ h(gap)
+ }
+ {
+ let body = entry.body()
+ assert(body.fields().keys().contains("children"), message: "论文应全部使用 `dual-heading` 作为标题")
+ let meta = body.children.last()
+ assert(repr(meta).starts-with("metadata"), message: "论文应全部使用 `dual-heading` 作为标题")
+ assert(meta.value.keys().contains("en"), message: "论文应全部使用 `dual-heading` 作为标题")
+ meta.value.en
+ }
+ },
+ )
+ box(width: 1fr, inset: (x: .25em), fill.at(entry.level - 1, default: fill.last()))
+ entry.page()
+ },
+ gap: 0pt,
+ ),
+ ),
+ )
+
+ // 显示目录
+ outline(title: none, depth: depth)
+}
pages/bachelor-outline-page.typ
@@ -0,0 +1,86 @@
+#import "../utils/invisible-heading.typ": invisible-heading
+#import "../utils/style.typ": 字号, 字体
+
+// 本科生中文目录
+#let bachelor-outline-page(
+ // documentclass 传入参数
+ twoside: false,
+ fonts: (:),
+ // 其他参数
+ depth: 4,
+ title: "目 录",
+ outlined: false,
+ title-vspace: 14pt,
+ title-text-args: auto,
+ // 引用页码字体与字号
+ reference-font: auto,
+ reference-size: 字号.小四,
+ // 字体与字号
+ font: auto,
+ size: (字号.四号, 字号.小四),
+ // 目录样式
+ above: (20pt, 14pt),
+ below: (14pt, 14pt),
+ indent: (0pt, 18pt, 28pt),
+ fill: (repeat([.], gap: 0.15em),),
+ gap: .3em,
+) = {
+ // 1. 默认参数
+ fonts = 字体 + fonts
+ if title-text-args == auto {
+ title-text-args = (font: fonts.黑体, size: 字号.小三)
+ }
+ if reference-font == auto {
+ reference-font = fonts.宋体
+ }
+ if font == auto {
+ font = (fonts.黑体, fonts.黑体, fonts.宋体)
+ }
+
+ // 2. 正式渲染
+ pagebreak(weak: true, to: if twoside { "odd" })
+
+ // 默认显示的字体
+ set text(font: reference-font, size: reference-size)
+
+ {
+ set align(center)
+ text(..title-text-args, title)
+ // 标记一个不可见的标题用于目录生成
+ invisible-heading(level: 1, outlined: outlined, title)
+ }
+
+ v(title-vspace)
+
+ // 目录样式
+ set outline(indent: level => indent.slice(0, calc.min(level + 1, indent.len())).sum())
+ show outline.entry: entry => block(
+ above: above.at(entry.level - 1, default: above.last()),
+ below: below.at(entry.level - 1, default: below.last()),
+ link(
+ entry.element.location(),
+ entry.indented(
+ none,
+ {
+ text(
+ font: font.at(entry.level - 1, default: font.last()),
+ size: size.at(entry.level - 1, default: size.last()),
+ {
+ if entry.prefix() not in (none, []) {
+ entry.prefix()
+ h(gap)
+ }
+ entry.body()
+ },
+ )
+ box(width: 1fr, inset: (x: .25em), fill.at(entry.level - 1, default: fill.last()))
+ entry.page()
+ },
+ gap: 0pt,
+ ),
+ ),
+ )
+
+ // 显示目录
+ outline(title: none, depth: depth)
+}
template/thesis.typ
@@ -1,5 +1,5 @@
-// #import "@preview/modern-xmu-thesis:0.0.1": documentclass // TODO: 上传至 Typst Universe 时取消本行注释
-#import "../lib.typ": documentclass // TODO: 上传至 Typst Universe 时删除本行
+// #import "@preview/modern-xmu-thesis:0.0.1": documentclass, dual-heading // TODO: 上传至 Typst Universe 时取消本行注释
+#import "../lib.typ": documentclass, dual-heading // TODO: 上传至 Typst Universe 时删除本行
#let (
// 布局函数
@@ -12,6 +12,8 @@
integrity,
abstract,
abstract-en,
+ outline-page,
+ outline-page-en,
) = documentclass(
twoside: true, // 双面模式,会加入空白页,便于打印
info: (
@@ -49,7 +51,6 @@
// 中文摘要
#abstract(
keywords: ("本科毕业论文", "厦门大学", "Typst"),
- outlined: true,
outline-title: "中文摘要",
)[
// 导入 LaTeX 图标
@@ -69,7 +70,6 @@
#abstract-en(
twoside: false,
keywords: ("Undergraduate Thesis", "Xiamen University", "Typst"),
- outlined: true,
outline-title: "Abstract",
)[
// 导入 LaTeX 图标
@@ -82,9 +82,11 @@
It is recommended to fully browse all the content in this template before use to fully understand how to use the template and the basic usage of Typst.
]
-// TODO: 中文目录页
+// 中文目录页
+#outline-page()
-// TODO: 英文目录页
+// 英文目录页
+#outline-page-en(twoside: false)
// ====== 正文部分 ======
#show: mainmatter
utils/dual-heading.typ
@@ -0,0 +1,55 @@
+// 实现双语标题
+
+// 清除 sequence 中的空元素\
+// 如果元素非 content 或者没有 children 字段,则直接返回\
+//
+// - it (): 需要清除的元素,应为 sequence
+// -> content, array
+#let trim-sequence(it) = {
+ if type(it) != content or not it.fields().keys().contains("children") {
+ return it
+ }
+ it.children.filter(it => it.fields().keys().len() > 0)
+}
+
+// 双语标题,只会显示中文部分
+//
+// 英文部分仅用于 `#outline-en()` 元数据查询,
+// `metadata` 将出现在 `heading.body` 末尾,
+// 可使用 `heading.body.children.last()` 获取
+//
+// `metadata` 结构为 `metadata(("en": content))`
+//
+// 用法:
+// ```typ
+// // 可以集中于一行
+// #dual-heading()[= 中文][= English]
+// // 也可以分开,对长标题更加美观
+// #dual-heading()[
+// = 很长很长的中文标题
+// ][
+// = A Very Loooooooong English Heading
+// ]
+// ```
+//
+// - zh (content): 中文标题
+// - en (content): 英文标题
+// -> content
+#let dual-heading(zh, en) = {
+ let zh-heading = trim-sequence(zh)
+ let en-heading = trim-sequence(en)
+
+ zh-heading = if type(zh-heading) == array {
+ zh-heading.at(0)
+ } else { zh-heading }
+ en-heading = if type(en-heading) == array {
+ en-heading.at(0)
+ } else { en-heading }
+
+ assert.eq(type(zh-heading), content, message: "中文标题应为 heading")
+ assert(repr(zh-heading).starts-with("heading"), message: "中文标题应为 heading")
+ assert.eq(type(en-heading), content, message: "英文标题应为 heading")
+ assert(repr(en-heading).starts-with("heading"), message: "英文标题应为 heading")
+
+ heading([#zh-heading.body#metadata(("en": en-heading.body))], depth: zh-heading.depth)
+}
lib.typ
@@ -6,7 +6,10 @@
#import "pages/bachelor-integrity.typ": bachelor-integrity
#import "pages/bachelor-abstract.typ": bachelor-abstract
#import "pages/bachelor-abstract-en.typ": bachelor-abstract-en
+#import "pages/bachelor-outline-page.typ": bachelor-outline-page
+#import "pages/bachelor-outline-page-en.typ": bachelor-outline-page-en
#import "utils/style.typ": 字体, 字号
+#import "utils/dual-heading.typ": dual-heading
// 使用函数闭包特性,通过 `documentclass` 函数类进行全局信息配置,然后暴露出拥有了全局配置的、具体的 `layouts` 和 `templates` 内部函数。
#let documentclass(
@@ -87,5 +90,17 @@
..args,
fonts: fonts + args.named().at("fonts", default: (:)),
),
+ // 中文目录页
+ outline-page: (..args) => bachelor-outline-page(
+ twoside: twoside,
+ ..args,
+ fonts: fonts + args.named().at("fonts", default: (:)),
+ ),
+ // 英文目录页
+ outline-page-en: (..args) => bachelor-outline-page-en(
+ twoside: twoside,
+ ..args,
+ fonts: fonts + args.named().at("fonts", default: (:)),
+ ),
)
}