// cph-render — curriculum lesson render package. // // PUBLIC ENTRY: `render-lesson(info, target, parts, heading-numbering)`. // // MODEL (ADR-0011). A build compiles a *template file* (e.g. `exports/student.typ`) // as the typst main, with the manifest injected via `--input manifest=`. // The template reads the manifest, loads each part's content, and calls // `render-lesson` here. Presentation (heading numbering) lives in the template, // not the manifest, not hardcoded-unreachable in this package — we only provide // the DEFAULT scheme (`default-heading-numbering`) for the template to use or // override. // // WHY THE TEMPLATE LOADS CONTENT, NOT US (the include-resolution finding): // typst resolves an `include`/`import` path relative to THE PACKAGE/FILE THE // include LEXICALLY APPEARS IN, and a package (`@local/cph-render`) has its OWN // virtual root. A dynamic `include` written inside this file resolves against // the PACKAGE root — even an absolute `/segments/x.typ` lands in the package // dir, NOT the engineering-file root (`--root`). Verified empirically. // Therefore the dynamic-include LOOP must live in the TEMPLATE (which lives at // `/exports/*.typ`, so `//.typ` resolves against // `--root`). `render-lesson` is content-in: it takes an ALREADY-ASSEMBLED // `parts` array of dicts and never includes anything itself. // // A part dict the template hands us: // `kind` plus that kind's content/scalar fields. Content field VALUES are // already-evaluated typst content (the template produced them via `include`). // // kind -> fields (MVP) — see `part-fields` below, which the template uses to // know what to load: // segment : textbook (content, required) // example : problem (content, required), solution (content, required), // source (string scalar, optional, from /element.toml) // lemma : stmt (content, required), proof (content, optional) // sop : sop (content, required) // // target -> show/hide (the (kind x target) render matrix; defaults per // ADR-0005/0008, derived internally — template passes only the target string): // student : example problem only (hide solution); lemma stmt only (hide // proof); segment textbook; sop shown. // teacher : everything shown. // unknown : rendered conservatively == student (show only required-public // fields). Never crashes. The "no render rule => warning" // diagnostic is the Rust side's job, not ours. #import "src/style.typ": base-style, default-heading-numbering, title-block, subtitle-block #import "src/elements/segment.typ": display-segment #import "src/elements/example.typ": display-example #import "src/elements/lemma.typ": display-lemma #import "src/elements/sop.typ": display-sop #import "src/elements/common.typ": example-counter, lemma-counter /// Per-kind field manifest, exported so a TEMPLATE knows exactly what to load /// for each part without hardcoding the matrix. For each kind: /// `content`: field names whose `/.typ` the template must /// `include` (assembling them into the part dict). /// `optional-content`: content fields that may be absent (template should /// probe before including; absence is fine). /// `scalars`: scalar field names the template reads from `/element.toml` /// (currently only example `source`). /// Keeping this here (not in the template) keeps kind->fields a single source of /// truth in the package; the template iterates it generically. #let part-fields = ( segment: (content: ("textbook",), optional-content: (), scalars: ()), example: (content: ("problem", "solution"), optional-content: (), scalars: ("source",)), lemma: (content: ("stmt",), optional-content: ("proof",), scalars: ()), sop: (content: ("sop",), optional-content: (), scalars: ()), ) /// Resolve a target string to the set of show/hide booleans. /// Unknown targets fall back to the conservative (student-like) profile. #let _flags-for(target) = { if target == "teacher" { (show-solution: true, show-proof: true, subtitle: [教师版讲义]) } else if target == "student" { (show-solution: false, show-proof: false, subtitle: [学生版讲义]) } else { // Unknown target: conservative — show only required-public fields. (show-solution: false, show-proof: false, subtitle: [讲义]) } } /// Render one part according to its `kind`. Unknown kinds render an inline /// note instead of crashing. #let _render-part(part, flags) = { let kind = part.at("kind", default: none) if kind == "segment" { display-segment(part) } else if kind == "example" { display-example(part, show-solution: flags.show-solution) } else if kind == "lemma" { display-lemma(part, show-proof: flags.show-proof) } else if kind == "sop" { display-sop(part) } else { // Don't crash on an unrecognised kind; surface it visibly instead. block( width: 100%, inset: 0.6em, fill: red.lighten(85%), stroke: red.darken(20%) + 0.6pt, radius: 0.4em, text(fill: red.darken(30%), weight: "bold")[ [未知部件类型 / unknown kind: #repr(kind)] ], ) } } /// THE ENTRY POINT — called by a template (`exports/*.typ`). /// /// - `info`: dict, e.g. (title: "…", author: "…"). `author` may be absent. /// Templates typically pass `manifest.at("info", default: (:))`. /// - `target`: string. MVP: "student" | "teacher". Unknown => conservative. /// Each template hardcodes its own target (student.typ => "student"). /// - `parts`: ordered array of part dicts, ALREADY ASSEMBLED by the template /// (content fields included, scalars read). See file header. /// - `heading-numbering`: array of per-level numbly pattern strings, e.g. /// `("{1:一}、", "{1:1}.{2:1}", "{1:1}.{2:1}.{3:1}")`. Presentation /// lives in the template (ADR-0011); the template passes its chosen /// scheme. Defaults to `default-heading-numbering` (the framework /// default) when the template omits it. #let render-lesson( info: (:), target: "student", parts: (), heading-numbering: default-heading-numbering, ) = { let title = info.at("title", default: []) let author = info.at("author", default: none) // `document` author wants a string/array; normalise the optional field. set document( title: title, author: if author == none { () } else { author }, ) show: base-style.with(heading-numbering: heading-numbering) // Reset shared counters so each rendered lesson numbers from 1. example-counter.update(0) lemma-counter.update(0) let flags = _flags-for(target) title-block() subtitle-block(flags.subtitle) // Render strictly in array order. for part in parts { _render-part(part, flags) } }