diff --git a/examples/TH-141/exports/student.typ b/examples/TH-141/exports/student.typ new file mode 100644 index 0000000..a33d25d --- /dev/null +++ b/examples/TH-141/exports/student.typ @@ -0,0 +1,79 @@ +// DEFAULT STUDENT TEMPLATE (framework default; ADR-0011). +// +// This is a *real, editable* file that lives in an engineering file at +// `exports/student.typ`. The framework compiles it AS THE MAIN FILE with the +// manifest injected: +// typst compile --root --input manifest= exports/student.typ +// +// It is intentionally self-contained (no shared helper import) so it can be +// copied verbatim into a new engineering file's `exports/`. Presentation — +// heading numbering — lives HERE (editable per engineering file), not in the +// manifest and not hardcoded in the cph-render package. +// +// WHY THE INCLUDE LOOP IS HERE AND NOT IN cph-render: typst resolves a dynamic +// `include` path relative to the file it lexically appears in, and a package has +// its own virtual root — an include inside cph-render would resolve against the +// PACKAGE, not the engineering root. A `//.typ` written HERE +// (this template lives under `--root`) resolves against `--root`. So the +// template loads content and hands cph-render an already-assembled `parts` array. +// +// OPEN CONTRACT POINT — optional-content presence. typst has no "does this file +// exist" primitive (a missing `include` is a hard compile error). So the +// template CANNOT probe disk the way the old Rust driver did for lemma `proof`. +// It relies on the manifest declaring which optional content fields are present, +// via a per-part `fields` array listing the content fields that exist on disk +// (the engine knows this — it walks the part dir). Required fields are loaded +// unconditionally; optional fields load only if listed in `fields`. If a part +// omits `fields`, optional content is skipped (conservative). The exact shape of +// this declaration is for the manifest/Rust contract to pin. + +#import "@local/cph-render:0.1.0": render-lesson, part-fields, default-heading-numbering + +// This template IS the student build, so the target is fixed. +#let target = "student" + +// Read the injected manifest (a path string relative to typst --root). +#let manifest = toml(sys.inputs.manifest) +#let info = manifest.at("info", default: (:)) +#let raw-parts = manifest.at("parts", default: ()) + +// Assemble each part: include its content fields (computed absolute paths, +// resolved against --root) and read scalar fields from /element.toml. +// `part-fields` (from cph-render) is the single source of truth for kind->fields. +#let parts = raw-parts.map(raw => { + let kind = raw.at("kind", default: none) + let path = raw.at("path", default: none) + let spec = part-fields.at(kind, default: (content: (), optional-content: (), scalars: ())) + // Which optional content fields are present on disk (manifest-declared). + let present = raw.at("fields", default: ()) + let part = (kind: kind) + + // Required content fields: /.typ (absolute, root-relative). + for field in spec.content { + part.insert(field, include "/" + path + "/" + field + ".typ") + } + // Optional content fields: only when the manifest says the file exists. + for field in spec.optional-content { + if field in present { + part.insert(field, include "/" + path + "/" + field + ".typ") + } + } + // Scalar fields come from /element.toml. + if spec.scalars.len() > 0 { + let element = toml("/" + path + "/element.toml") + for field in spec.scalars { + let v = element.at(field, default: none) + if v != none and v != "" { part.insert(field, v) } + } + } + part +}) + +// Presentation: per-level heading numbering. Default (一、 / 1.1 / 1.1.1) comes +// from cph-render; override here per engineering file if desired. +#render-lesson( + info: info, + target: target, + parts: parts, + heading-numbering: default-heading-numbering, +) diff --git a/examples/TH-141/exports/teacher.typ b/examples/TH-141/exports/teacher.typ new file mode 100644 index 0000000..b976b1f --- /dev/null +++ b/examples/TH-141/exports/teacher.typ @@ -0,0 +1,62 @@ +// DEFAULT TEACHER TEMPLATE (framework default; ADR-0011). +// +// Lives in an engineering file at `exports/teacher.typ`. Compiled AS MAIN with +// the manifest injected: +// typst compile --root --input manifest= exports/teacher.typ +// +// Self-contained (copyable into an engineering file). Identical to student.typ +// except the fixed target is "teacher" (so the (kind x target) render matrix in +// cph-render shows solutions and proofs). See student.typ for the full notes on +// why the include loop lives here (package virtual-root resolution) and on the +// OPEN optional-content (`fields`) contract point. + +#import "@local/cph-render:0.1.0": render-lesson, part-fields, default-heading-numbering + +// This template IS the teacher build, so the target is fixed. +#let target = "teacher" + +// Read the injected manifest (a path string relative to typst --root). +#let manifest = toml(sys.inputs.manifest) +#let info = manifest.at("info", default: (:)) +#let raw-parts = manifest.at("parts", default: ()) + +// Assemble each part: include its content fields (computed absolute paths, +// resolved against --root) and read scalar fields from /element.toml. +// `part-fields` (from cph-render) is the single source of truth for kind->fields. +#let parts = raw-parts.map(raw => { + let kind = raw.at("kind", default: none) + let path = raw.at("path", default: none) + let spec = part-fields.at(kind, default: (content: (), optional-content: (), scalars: ())) + // Which optional content fields are present on disk (manifest-declared). + let present = raw.at("fields", default: ()) + let part = (kind: kind) + + // Required content fields: /.typ (absolute, root-relative). + for field in spec.content { + part.insert(field, include "/" + path + "/" + field + ".typ") + } + // Optional content fields: only when the manifest says the file exists. + for field in spec.optional-content { + if field in present { + part.insert(field, include "/" + path + "/" + field + ".typ") + } + } + // Scalar fields come from /element.toml. + if spec.scalars.len() > 0 { + let element = toml("/" + path + "/element.toml") + for field in spec.scalars { + let v = element.at(field, default: none) + if v != none and v != "" { part.insert(field, v) } + } + } + part +}) + +// Presentation: per-level heading numbering. Default (一、 / 1.1 / 1.1.1) comes +// from cph-render; override here per engineering file if desired. +#render-lesson( + info: info, + target: target, + parts: parts, + heading-numbering: default-heading-numbering, +) diff --git a/examples/TH-141/manifest.toml b/examples/TH-141/manifest.toml index 2266562..3376bf0 100644 --- a/examples/TH-141/manifest.toml +++ b/examples/TH-141/manifest.toml @@ -162,16 +162,20 @@ path = "segments/各模型对水的预测对照" kind = "segment" path = "segments/算不准背后的真实物理" -# Export targets (ADR-0009): each is a build producing a typed artifact, with -# file-resident presentation config that overrides the framework defaults. -# Heading numbering is given explicitly per level (level-1 一、, level-2 1.1, -# level-3 1.1.1) — overriding the render package's default via the in-file path. +# Export targets (ADR-0009/0011): each target is a build producing a typed +# artifact, run as an ordered list of typed steps. A `typst-compile` step names a +# real template file under `exports/` — the framework compiles it as the main +# file with the manifest injected, and the template reads the manifest and renders +# the lesson. Presentation (heading numbering, styling) lives in the template +# file, NOT here (ADR-0011 moved it off the manifest). [targets.student] -artifact = "single-file" -[targets.student.numbering] -heading = ["{1:一}、", "{1:1}.{2:1}", "{1:1}.{2:1}.{3:1}"] +artifact = { type = "single-file", filepath = "build/student.pdf" } +[[targets.student.steps]] +type = "typst-compile" +template = "exports/student.typ" [targets.teacher] -artifact = "single-file" -[targets.teacher.numbering] -heading = ["{1:一}、", "{1:1}.{2:1}", "{1:1}.{2:1}.{3:1}"] +artifact = { type = "single-file", filepath = "build/teacher.pdf" } +[[targets.teacher.steps]] +type = "typst-compile" +template = "exports/teacher.typ"