- 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
274 lines
6.9 KiB
Typst
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
|
|
}
|