// 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, )