Files
mytoolkit/templates/md-to-pdf/textbook/template.typ
T
Zhengshou Lai 11f5635c94 feat(template): themed blockquotes, references, chapter-based numbering for textbook
- Add themed blockquote styles (课堂小实验, 关键洞察, etc.) via quote block
- Add #references() wrapper for bibliography formatting
- Implement chapter-based figure/table numbering (1-1, 2-1, ...)
- Start each chapter on a new page (except first)
- Increase list spacing to match long-form reading rhythm
- Fix first-line indent to apply on all paragraphs
2026-05-08 13:53:31 +08:00

274 lines
6.9 KiB
Typst

// 教材模板 —— 定理/代码/目录 + 封面页 + 三线表
#import "@preview/bookly:3.1.0": *
#import "@preview/great-theorems:0.1.2": *
#import "@preview/codly:1.3.0": *
#import "@preview/showybox:2.0.4": showybox
// ── helpers ──────────────────────────────────────────────────────────────
#let content-to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
content-to-string(content.body)
} else if content == [ ] {
" "
} else {
""
}
}
// ── blockquote themes ────────────────────────────────────────────────────
#let blockquote-themes = (
("课堂小实验", rgb("#F0FDF4"), rgb("#16A34A")),
("关键洞察", rgb("#FFFBEB"), rgb("#D97706")),
("工程警示", rgb("#FEF2F2"), rgb("#DC2626")),
("前沿瞭望", rgb("#F3E8FF"), rgb("#9333EA")),
("历史视角", rgb("#FEF3C7"), rgb("#B45309")),
("你知道吗", rgb("#ECFEFF"), rgb("#0891B2")),
)
#let themed-blockquote(body) = {
let body-str = content-to-string(body)
let theme = blockquote-themes.find(t => body-str.contains(t.at(0)))
let (fill-color, stroke-color) = if theme != none {
(theme.at(1), theme.at(2))
} else {
(rgb("#F1F5F9"), rgb("#64748B"))
}
block(
fill: fill-color,
stroke: (left: 4pt + stroke-color),
inset: (x: 14pt, y: 10pt),
radius: (right: 4pt),
spacing: 1.2em,
body,
)
}
#let primary = rgb("#1F4E79")
#let secondary = rgb("#2C3E50")
#let accent = rgb("#3467A8")
#let light = rgb("#96A0AA")
#let rule-blue = accent.lighten(46%)
// Filled mathblocks: inset so fill does not hug text; radius slightly larger than codly (4pt)
#let _mathblock-inset = (x: 14pt, y: 12pt)
#let _mathblock-radius = 5pt
// Theorem-like environments (great-theorems)
#let theorem = mathblock(
blocktitle: "Theorem",
fill: rgb("#E8F0FE"),
inset: _mathblock-inset,
radius: _mathblock-radius,
)
#let lemma = mathblock(
blocktitle: "Lemma",
fill: rgb("#F3E8FF"),
inset: _mathblock-inset,
radius: _mathblock-radius,
)
#let proof = proofblock(
inset: _mathblock-inset,
radius: _mathblock-radius,
)
#let example = mathblock(
blocktitle: "Example",
fill: rgb("#F0FDF4"),
inset: _mathblock-inset,
radius: _mathblock-radius,
)
// Uncounted block: titles must use `title: ...`, not a positional string (see great-theorems README).
#let exercise = mathblock(
blocktitle: "Exercise",
prefix: [],
titlix: title => [*习题*~#title~#h(0.25em)],
fill: rgb("#FEF3C7"),
inset: (x: 14pt, y: 13pt),
radius: _mathblock-radius,
)
#let cover-page(title, subtitle, date) = {
page(margin: (top: 3cm, bottom: 3cm, left: 3cm, right: 3cm))[
#align(center + horizon)[
#v(2cm)
#text(size: 28pt, weight: "bold", fill: primary, title)
#if subtitle != none {
v(0.8cm)
text(size: 16pt, fill: accent, subtitle)
}
#if date != none {
v(2cm)
text(size: 12pt, fill: light, date)
}
]
]
}
#let three-line-table(..args) = {
let kwargs = args.named()
let children = args.pos()
let new_children = ()
let header_seen = false
for child in children {
if child.func() == table.header {
new_children.push(table.hline(stroke: 1.25pt + rule-blue))
new_children.push(child)
header_seen = true
} else if header_seen and child.func() == table.hline {
new_children.push(table.hline(stroke: 0.75pt + rule-blue))
header_seen = false
} else {
new_children.push(child)
}
}
new_children.push(table.hline(stroke: 1.25pt + rule-blue))
kwargs.stroke = none
kwargs.inset = (x: 8pt, y: 5pt)
table(..new_children, ..kwargs)
}
// Reference-list block: smaller text, hanging indent, tighter rhythm.
// Wrap a 参考文献 / References section's body via `#references[ ... ]`.
#let references(body) = block({
set text(size: 9.5pt)
set par(
justify: true,
leading: 0.7em,
spacing: 0.6em,
first-line-indent: 0pt,
hanging-indent: 1.5em,
)
body
})
#let textbook(
title: none,
subtitle: none,
date: none,
body,
) = {
if title != none {
cover-page(title, subtitle, date)
pagebreak()
}
set text(
font: (
"Cambria",
"PingFang SC",
),
size: 11pt,
lang: "zh",
)
show raw: set text(font: (
"Courier New",
"Menlo",
"Monaco",
"SimSun",
))
// Long-form reading: most generous body rhythm in this family
set par(
justify: true,
leading: 0.86em,
spacing: 1.2em,
first-line-indent: (amount: 2em, all: true),
)
show: codly-init
codly(
stroke: 0.5pt + rule-blue,
fill: rgb("#F8F9FA"),
zebra-fill: none,
lang-stroke: none,
lang-fill: light.lighten(60%),
radius: 4pt,
)
show: great-theorems-init
set heading(numbering: "1.1")
// 按章编号:每章开始时重置图片/表格计数器
set figure(numbering: (..nums) => {
let h = counter(heading).get()
let ch = if h.len() > 0 { h.first() } else { 1 }
numbering("1-1", ch, nums.pos().first())
})
// List rhythm: spacing ≥ leading (0.86em), slightly looser than body for long-form reading
set list(indent: 1.2em, body-indent: 0.45em, spacing: 0.98em)
set enum(indent: 1.2em, body-indent: 0.45em, spacing: 0.98em)
// 每章从新页开始(第一章除外,因为前面已有 pagebreak)
let first-chapter = state("first-chapter", true)
show heading.where(level: 1): it => {
counter(figure.where(kind: image)).update(0)
counter(figure.where(kind: table)).update(0)
context if not first-chapter.get() {
pagebreak()
}
first-chapter.update(false)
v(1.45em)
text(size: 18pt, weight: "bold", fill: primary, it)
v(0.55em)
}
show heading.where(level: 2): it => {
v(1.1em)
text(size: 14pt, weight: "bold", fill: accent, it)
v(0.5em)
}
show heading.where(level: 3): it => {
v(0.8em)
text(size: 12pt, weight: "bold", fill: secondary, it)
v(0.4em)
}
// Blockquote → themed block
show quote.where(block: true): it => themed-blockquote(it.body)
set page(
paper: "a4",
margin: (top: 2.5cm, bottom: 2.5cm, left: 2.5cm, right: 2.5cm),
header: context {
let h = query(selector(heading.where(level: 1))).find(it => it.location().page() == here().page())
if h != none {
align(right)[
#text(size: 9pt, fill: light)[
#h.body
]
]
}
},
footer: context {
align(center)[
#text(size: 9pt, fill: light)[
#counter(page).display("1")
]
]
},
)
show link: it => text(fill: accent, it)
outline(title: "目录", depth: 2)
pagebreak()
body
}