Commit d7979b2

HPCesia <me@hpcesia.com>
2025-03-20 06:56:37
feat: 双语目录页
1 parent 521eda1
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: (:)),
+    ),
   )
 }