forked from EduCraft/curriculum-project-hub
feat(checker): compile template as main, augmented manifest closes optional-content (WU-D', ADR-0011)
Replace generated-driver compilation with the template model.
cph-typst:
- driver.rs DELETED (generate_driver, static-include driver, numbering-threading).
- New manifest.rs: build_augmented_manifest(&Lesson) emits a TOML doc with [info]
+ ordered [[parts]] each carrying a `fields` array = the kind's content fields
whose <root>/<path>/<field>.typ exists on disk. Reuses cph-schema's
content_field_names (new dep; no cycle). This closes the OPEN point WU-C'
surfaced: typst has no file-exists primitive, so the engine (which has disk
access) tells the template which optional content (lemma proof) is present.
- World main = the real engineering-file template (<root>/<template>); the
augmented manifest is served as an in-memory virtual file at /.cph/manifest.toml
(never written to the tree), injected via sys.inputs.manifest. render_dir kept
only for @local/cph-render + vendored @preview/numbly resolution.
- Artifact handling: SingleFile+TypstCompile compiles; FileTree, shell-only, and
undeclared-target are blocking "deferred/not-declared" diagnostics (ADR-0011).
- Engine public signatures unchanged; LessonWorld::new takes template+manifest_src.
cph-check: no source change needed (only uses lesson.targets/target.name +
unchanged Engine methods); pipeline + Legal alignment intact.
Fixtures: mini gets exports/{student,teacher}.typ (from render/templates/) + v2
manifest + a second proof-less lemma to exercise optional content. render-stub
retired (tests use the real render/). Through-template PDFs offline: student 44KB,
teacher 56KB; proof-less lemma compiles clean (optional-content confirmed).
Workspace: fmt + clippy -D warnings + all tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,45 @@
|
||||
//! [`LessonWorld`] — a typst [`World`] over an engineering-file directory tree
|
||||
//! plus an in-memory generated driver and a disk-mounted `cph-render` package.
|
||||
//! plus an in-memory augmented manifest and a disk-mounted `cph-render` package.
|
||||
//!
|
||||
//! ## Model (ADR-0011)
|
||||
//!
|
||||
//! The `main` file is the target's **template** (e.g. `<root>/exports/student.typ`),
|
||||
//! a *real* file under the lesson root — no framework-generated driver. The
|
||||
//! template `toml()`-reads an injected manifest and `include`s each part's
|
||||
//! content via computed root-relative paths. The engine injects the manifest as
|
||||
//! a virtual file (see below) and sets `sys.inputs.manifest` to its vpath.
|
||||
//!
|
||||
//! ## File resolution
|
||||
//!
|
||||
//! - **The driver** (`main()`): mounted in-memory at `VirtualRoot::Project` +
|
||||
//! vpath `/.cph/driver-<target>.typ`. `source()` returns it from the field;
|
||||
//! it is never written to disk. Its root-relative `include "/segments/…"`
|
||||
//! paths therefore anchor at the lesson root.
|
||||
//! - **Project files**: any `FileId` with `VirtualRoot::Project` is read from
|
||||
//! `root.join(vpath.realize)`. Used for content `.typ`, images, `toml()`, etc.
|
||||
//! - **`@local/cph-render`**: a `FileId` whose root is the render package spec
|
||||
//! is read from `render_dir.join(vpath.realize)`. This avoids installing the
|
||||
//! package into the typst local-package directory (MVP: no environment
|
||||
//! coupling).
|
||||
//! - **The template** (`main()`): a `FileId` with `VirtualRoot::Project` whose
|
||||
//! vpath is the template's path under the lesson root (e.g. `/exports/student.typ`).
|
||||
//! It is read from disk like any other project file; its `include "/<part>/…"`
|
||||
//! paths anchor at the lesson root (`--root`).
|
||||
//! - **The augmented manifest**: served in-memory at `VirtualRoot::Project` +
|
||||
//! vpath [`MANIFEST_VPATH`] (`/.cph/manifest.toml`). It is **never** written to
|
||||
//! disk — the engine builds it (original manifest + a per-part `fields` array)
|
||||
//! and the template reads it via `toml(sys.inputs.manifest)`.
|
||||
//! - **Project files**: any other `FileId` with `VirtualRoot::Project` is read
|
||||
//! from `root.join(vpath.realize)`. Used for content `.typ`, images,
|
||||
//! `element.toml` `toml()`, etc.
|
||||
//! - **`@local/cph-render`**: a `FileId` whose root is the render package spec is
|
||||
//! read from `render_dir.join(vpath.realize)`, avoiding installing the package
|
||||
//! into the typst local-package directory.
|
||||
//! - **`@preview/<name>:<version>`**: resolved offline from the in-repo vendored
|
||||
//! preview tree at `<render_dir>/vendor/typst-packages/preview/<name>/<version>/`
|
||||
//! (mirroring typst's preview-cache layout). The render package imports
|
||||
//! `@preview/numbly:0.1.0` (per-level heading numbering); reading it from the
|
||||
//! vendored dir keeps compilation network-free with no `typst-kit` package
|
||||
//! download. This is general — any `@preview/*` resolves from the vendored
|
||||
//! preview dir; numbly 0.1.0 is the only one currently present.
|
||||
//! preview tree at `<render_dir>/vendor/typst-packages/preview/<name>/<version>/`.
|
||||
//! The render package imports `@preview/numbly:0.1.0`; reading it from the
|
||||
//! vendored dir keeps compilation network-free.
|
||||
//! - Any *other* `@package` namespace yields a `NotFound` error.
|
||||
//!
|
||||
//! Sources are cached in a `Mutex<HashMap<FileId, Source>>` so repeated
|
||||
//! accesses during one compilation are cheap, as the `World` contract expects.
|
||||
//! Sources are cached in a `Mutex<HashMap<FileId, Source>>` so repeated accesses
|
||||
//! during one compilation are cheap, as the `World` contract expects.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use typst::diag::{FileError, FileResult};
|
||||
use typst::foundations::{Bytes, Datetime, Duration};
|
||||
use typst::foundations::{Bytes, Datetime, Dict, Duration, Value};
|
||||
use typst::syntax::package::{PackageSpec, PackageVersion};
|
||||
use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
|
||||
use typst::text::{Font, FontBook};
|
||||
@@ -38,9 +47,15 @@ use typst::utils::LazyHash;
|
||||
use typst::{Library, LibraryExt, World};
|
||||
use typst_kit::fonts::FontStore;
|
||||
|
||||
use crate::driver::DRIVER_DIR;
|
||||
/// Root-relative vpath the augmented manifest is served at (in-memory only).
|
||||
///
|
||||
/// A **leading slash** is essential: the template lives under `exports/`, and
|
||||
/// `toml(sys.inputs.manifest)` resolves a relative path against the template's
|
||||
/// own directory — a bare name would miss. A root-relative absolute path anchors
|
||||
/// at `--root` (the lesson root) regardless of where the template sits.
|
||||
pub const MANIFEST_VPATH: &str = "/.cph/manifest.toml";
|
||||
|
||||
/// The package spec the driver imports and the World mounts from `render_dir`.
|
||||
/// The package spec the template imports and the World mounts from `render_dir`.
|
||||
pub fn render_package_spec() -> PackageSpec {
|
||||
PackageSpec {
|
||||
namespace: "local".into(),
|
||||
@@ -61,11 +76,13 @@ pub struct LessonWorld {
|
||||
render_dir: PathBuf,
|
||||
/// The render package spec (`@local/cph-render:0.1.0`).
|
||||
render_spec: PackageSpec,
|
||||
/// FileId of the in-memory driver entrypoint.
|
||||
/// FileId of the template entrypoint (a real file under `root`).
|
||||
main: FileId,
|
||||
/// The driver source, parsed once.
|
||||
driver_source: Source,
|
||||
/// Standard library (default configuration).
|
||||
/// FileId of the in-memory augmented manifest.
|
||||
manifest_id: FileId,
|
||||
/// The augmented-manifest source (in-memory; never on disk).
|
||||
manifest_source: Source,
|
||||
/// Standard library, with `sys.inputs.manifest` set.
|
||||
library: LazyHash<Library>,
|
||||
/// Shared font store (book + lazily-loaded fonts).
|
||||
fonts: Arc<FontStore>,
|
||||
@@ -74,27 +91,42 @@ pub struct LessonWorld {
|
||||
}
|
||||
|
||||
impl LessonWorld {
|
||||
/// Build a world. `driver_src` is the generated entrypoint text; it is
|
||||
/// mounted in-memory at `/.cph/driver-<target>.typ` under the project root.
|
||||
/// Build a world whose `main` is the template at `<root>/<template>` and
|
||||
/// whose injected manifest is `manifest_src` (served virtually at
|
||||
/// [`MANIFEST_VPATH`], with `sys.inputs.manifest` pointing there).
|
||||
///
|
||||
/// `template` is the lesson-root-relative template path (e.g.
|
||||
/// `exports/student.typ`), taken from the target's `Step::TypstCompile`.
|
||||
pub fn new(
|
||||
root: PathBuf,
|
||||
render_dir: PathBuf,
|
||||
target: &str,
|
||||
driver_src: String,
|
||||
template: &Path,
|
||||
manifest_src: String,
|
||||
fonts: Arc<FontStore>,
|
||||
) -> Self {
|
||||
let driver_vpath = VirtualPath::new(format!("/{DRIVER_DIR}/driver-{target}.typ"))
|
||||
.expect("driver vpath is a valid virtual path");
|
||||
let main = FileId::new(RootedPath::new(VirtualRoot::Project, driver_vpath));
|
||||
let driver_source = Source::new(main, driver_src);
|
||||
let main_vpath = VirtualPath::new(format!("/{}", path_to_forward_slash(template)))
|
||||
.expect("template vpath is a valid virtual path");
|
||||
let main = FileId::new(RootedPath::new(VirtualRoot::Project, main_vpath));
|
||||
|
||||
let manifest_vpath =
|
||||
VirtualPath::new(MANIFEST_VPATH).expect("manifest vpath is a valid virtual path");
|
||||
let manifest_id = FileId::new(RootedPath::new(VirtualRoot::Project, manifest_vpath));
|
||||
let manifest_source = Source::new(manifest_id, manifest_src);
|
||||
|
||||
// Inject `sys.inputs.manifest = "/.cph/manifest.toml"` so the template's
|
||||
// `toml(sys.inputs.manifest)` reads the augmented manifest.
|
||||
let mut inputs = Dict::new();
|
||||
inputs.insert("manifest".into(), Value::Str(MANIFEST_VPATH.into()));
|
||||
let library = Library::builder().with_inputs(inputs).build();
|
||||
|
||||
Self {
|
||||
root,
|
||||
render_dir,
|
||||
render_spec: render_package_spec(),
|
||||
main,
|
||||
driver_source,
|
||||
library: LazyHash::new(Library::default()),
|
||||
manifest_id,
|
||||
manifest_source,
|
||||
library: LazyHash::new(library),
|
||||
fonts,
|
||||
sources: Mutex::new(HashMap::new()),
|
||||
}
|
||||
@@ -131,7 +163,8 @@ impl LessonWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read raw bytes for `id` from disk (driver excluded — handled by callers).
|
||||
/// Read raw bytes for `id` from disk (virtual files excluded — handled by
|
||||
/// callers).
|
||||
fn read_bytes(&self, id: FileId) -> FileResult<Vec<u8>> {
|
||||
let path = self.resolve(id)?;
|
||||
std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))
|
||||
@@ -152,8 +185,8 @@ impl World for LessonWorld {
|
||||
}
|
||||
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
if id == self.main {
|
||||
return Ok(self.driver_source.clone());
|
||||
if id == self.manifest_id {
|
||||
return Ok(self.manifest_source.clone());
|
||||
}
|
||||
// Cache hit?
|
||||
if let Some(src) = self.sources.lock().expect("sources mutex").get(&id) {
|
||||
@@ -170,8 +203,8 @@ impl World for LessonWorld {
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
if id == self.main {
|
||||
return Ok(Bytes::from_string(self.driver_source.text().to_string()));
|
||||
if id == self.manifest_id {
|
||||
return Ok(Bytes::from_string(self.manifest_source.text().to_string()));
|
||||
}
|
||||
let bytes = self.read_bytes(id)?;
|
||||
Ok(Bytes::new(bytes))
|
||||
@@ -186,3 +219,18 @@ impl World for LessonWorld {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a relative `Path` as a forward-slash string, dropping any leading
|
||||
/// `./` or `/` and ignoring `..`. UTF-8 segments kept verbatim.
|
||||
fn path_to_forward_slash(path: &Path) -> String {
|
||||
let mut out = String::new();
|
||||
for comp in path.components() {
|
||||
if let Component::Normal(s) = comp {
|
||||
if !out.is_empty() {
|
||||
out.push('/');
|
||||
}
|
||||
out.push_str(&s.to_string_lossy());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user