forked from bai/curriculum-project-hub
feat(render): rebuild typst render package cph-render (WU-2)
Single entry `display(info, target, parts)` over an ordered parts array, the frozen contract the generated typst driver calls. Per-kind dispatch for segment/example/lemma/sop; student/teacher matrix derived internally from `target` (student hides example solution + lemma proof; teacher shows all). Content-field values are consumed as already-evaluated content (driver `include`s them ⇒ module body, ADR-0006), never imported/stringified. Optional fields (lemma proof, example source) handled gracefully; unknown kind/target degrade without crashing. Zero @preview deps (CI-robust); CJK + math styled. Smoke compiles to non-empty student/teacher PDFs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// Shared building blocks for element renderers: counters, tag lines, framed
|
||||
// boxes, divider lines. Zero external deps (no fontawesome/showybox) — tags use
|
||||
// plain bold text labels instead of icon glyphs.
|
||||
|
||||
#import "../fonts.typ"
|
||||
#import "../style.typ": no-indent
|
||||
|
||||
/// Counters for numbered elements. Shared across the package so numbering is
|
||||
/// continuous across the lesson regardless of which file renders a part.
|
||||
#let example-counter = counter("cph-example")
|
||||
#let lemma-counter = counter("cph-lemma")
|
||||
|
||||
/// A bold label line introducing a section of an element body, e.g.
|
||||
/// "例题 1" or "解析". Starts flush-left (cancels paragraph indent).
|
||||
#let tag-line(label) = {
|
||||
no-indent
|
||||
set text(font: fonts.sans, weight: "bold")
|
||||
label
|
||||
h(1em / 3)
|
||||
}
|
||||
|
||||
/// A faint horizontal divider between sections inside an element box.
|
||||
#let divider(color) = block(spacing: 1em, {
|
||||
set align(center)
|
||||
line(length: 100%, stroke: color.darken(20%) + 0.5pt)
|
||||
})
|
||||
|
||||
/// A framed, tinted, breakable box wrapping an element's content.
|
||||
/// `hue` selects the accent color family (degrees on the oklch hue wheel).
|
||||
#let element-box(hue, body) = {
|
||||
let theme = oklch(70%, 0.08, hue * 1deg)
|
||||
block(
|
||||
inset: (x: 1.5em, y: 1em),
|
||||
breakable: true,
|
||||
width: 100%,
|
||||
fill: theme.lighten(70%).transparentize(50%),
|
||||
stroke: theme.darken(60%) + 0.8pt,
|
||||
radius: 0.5em,
|
||||
{
|
||||
set par(first-line-indent: 2em, spacing: 1em)
|
||||
body
|
||||
parbreak()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The accent hue per element kind (keeps the four kinds visually distinct).
|
||||
#let hue-of = (
|
||||
example: 250,
|
||||
lemma: 150,
|
||||
segment: 90,
|
||||
sop: 30,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
// example: a worked example / problem.
|
||||
//
|
||||
// Contract fields:
|
||||
// content: `problem` (required), `solution` (required)
|
||||
// scalar: `source` (optional string)
|
||||
//
|
||||
// Numbered "例题 N" via the shared example-counter.
|
||||
// student: show `problem`, HIDE `solution`.
|
||||
// teacher: show both.
|
||||
// `source`, when present, is shown as a small annotation on the problem line.
|
||||
|
||||
#import "../fonts.typ"
|
||||
#import "common.typ": element-box, tag-line, divider, example-counter, hue-of
|
||||
|
||||
#let display-example(part, show-solution: true) = {
|
||||
let theme = oklch(70%, 0.08, hue-of.example * 1deg)
|
||||
example-counter.step()
|
||||
|
||||
element-box(hue-of.example, {
|
||||
// Problem
|
||||
tag-line(context [例题 #example-counter.display()])
|
||||
let source = part.at("source", default: none)
|
||||
if source != none and source != "" {
|
||||
text(size: 0.85em, fill: gray.darken(30%))[(来源:#source)]
|
||||
h(0.3em)
|
||||
}
|
||||
set text(font: fonts.accent)
|
||||
part.problem
|
||||
|
||||
// Solution (teacher-only by default)
|
||||
if show-solution {
|
||||
divider(theme)
|
||||
tag-line[解析]
|
||||
set text(font: fonts.accent)
|
||||
part.solution
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// lemma: a stated result, optionally with a proof.
|
||||
//
|
||||
// Contract fields:
|
||||
// content: `stmt` (required), `proof` (optional)
|
||||
// scalar: none
|
||||
//
|
||||
// Numbered "定理 N" via the shared lemma-counter.
|
||||
// student: show `stmt`, HIDE `proof`.
|
||||
// teacher: show both (when `proof` is present).
|
||||
// `proof` may be absent (key omitted) — handled gracefully.
|
||||
|
||||
#import "../fonts.typ"
|
||||
#import "common.typ": element-box, tag-line, divider, lemma-counter, hue-of
|
||||
|
||||
#let display-lemma(part, show-proof: true) = {
|
||||
let theme = oklch(70%, 0.08, hue-of.lemma * 1deg)
|
||||
lemma-counter.step()
|
||||
|
||||
element-box(hue-of.lemma, {
|
||||
// Statement
|
||||
tag-line(context [定理 #lemma-counter.display()])
|
||||
set text(font: fonts.accent)
|
||||
part.stmt
|
||||
|
||||
// Proof: only when requested AND actually provided.
|
||||
let proof = part.at("proof", default: none)
|
||||
if show-proof and proof != none {
|
||||
divider(theme)
|
||||
tag-line[证明]
|
||||
set text(font: fonts.accent)
|
||||
proof
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// segment: a stretch of textbook prose.
|
||||
//
|
||||
// Contract fields:
|
||||
// content: `textbook` (required)
|
||||
// scalar: none
|
||||
//
|
||||
// Rendered identically for all targets (textbook is always public). Not boxed
|
||||
// or numbered — it is the connective body text of the lesson.
|
||||
|
||||
/// Render a segment part. `show-textbook` is always true in MVP but kept for
|
||||
/// symmetry with the other kinds' show/hide flags.
|
||||
#let display-segment(part, show-textbook: true) = {
|
||||
if show-textbook {
|
||||
part.textbook
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// sop: a "standard operating procedure" — a methods/steps block.
|
||||
//
|
||||
// Contract fields:
|
||||
// content: `sop` (required)
|
||||
// scalar: none
|
||||
//
|
||||
// Shown for all targets. Lightly boxed for visual separation; not numbered.
|
||||
|
||||
#import "common.typ": element-box, tag-line, hue-of
|
||||
|
||||
/// Render a sop part.
|
||||
#let display-sop(part, show-sop: true) = {
|
||||
if show-sop {
|
||||
element-box(hue-of.sop, {
|
||||
tag-line[方法]
|
||||
part.sop
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user