// DEFAULT STUDENT TEMPLATE (framework default; ADR-0011, outline shape ADR-0029). // // 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 `outline` // array (elements interleaved with section headings, ADR-0029). // // 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-element `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 an element // 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-outline = manifest.at("outline", default: ()) // Assemble each outline entry: // - an "element" entry: 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. // - a "section" entry (ADR-0029): pass its title/depth straight through — no // content to load, it is a heading. #let outline = raw-outline.map(raw => { if raw.at("type", default: "element") == "section" { ( entry-type: "section", kind: raw.at("kind", default: none), title: raw.at("title", default: ""), depth: raw.at("depth", default: 1), ) } else { 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 entry = (entry-type: "element", kind: kind) // Required content fields: /.typ (absolute, root-relative). for field in spec.content { entry.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 { entry.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 != "" { entry.insert(field, v) } } } entry } }) // 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, outline: outline, heading-numbering: default-heading-numbering, )