diff --git a/templates/md-to-pdf/textbook/template.typ b/templates/md-to-pdf/textbook/template.typ index 395dc67..e2e7bba 100644 --- a/templates/md-to-pdf/textbook/template.typ +++ b/templates/md-to-pdf/textbook/template.typ @@ -5,6 +5,51 @@ #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") @@ -92,6 +137,20 @@ 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, @@ -124,7 +183,7 @@ justify: true, leading: 0.86em, spacing: 1.2em, - first-line-indent: 2em, + first-line-indent: (amount: 2em, all: true), ) show: codly-init @@ -140,12 +199,29 @@ show: great-theorems-init set heading(numbering: "1.1") - // Same 11pt body as default: match list rhythm to default template - set list(indent: 1.2em, body-indent: 0.45em, spacing: 0.62em) - set enum(indent: 1.2em, body-indent: 0.45em, spacing: 0.62em) + // 按章编号:每章开始时重置图片/表格计数器 + 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) - // Vertical rhythm matches manual (long-form); font sizes differ 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) @@ -163,6 +239,9 @@ 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),