forked from EduCraft/curriculum-project-hub
feat(checker): cph-typst — embed typst 0.15, driver-gen, compile, PDF (WU-4)
The heart of the pipeline. Embeds the typst 0.15 compiler as a library to compile-check a lesson and export a PDF, with diagnostics mapped back to source spans (the product's value). - LessonWorld: a typst World over the engineering-file directory tree. vpath = real relative path (ADR-0007); the per-target driver is mounted in-memory as `main` (.cph/driver-<target>.typ, never written to disk); @local/cph-render is resolved from render_dir (CPH_RENDER_DIR / with_render_dir), no package install or network. Fonts via typst-kit FontStore (system CJK). - Driver generation: from the manifest, per target — static root-relative `include` of each content file (⇒ module body content, ADR-0006), assembled into the frozen `display(info, target, parts)` call; optional lemma proof emitted only if proof.typ exists; example `source` scalar threaded through. - Diagnostics: typst SourceDiagnostic (errors + warnings) → cph_diag::Diagnostic. 0.15 DiagSpan handled via span.id() + WorldExt::range → file-relative SourceSpan with 1-based line/col; driver-internal spans re-pointed with a "generated driver" hint, never dropped. typst hints/trace carried through. - Engine API: new / with_render_dir / compile_check / build_pdf. - 6 tests incl. build_pdf_with_real_render — full World→driver→cph-render→PDF producing real PDF-1.7 output (student 42KB / teacher 54KB). clippy/fmt clean. Also fixes render/typst.toml `compiler = ">=0.15.0"` → `"0.15.0"`: typst 0.15's manifest parser rejects a `>=` comparator on the compiler field (the 0.14 CLU WU-2 tested against was lenient). This unblocks loading the real render package. Pinned: typst/typst-pdf/typst-syntax/typst-library/typst-layout/typst-kit =0.15.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+138
-6
@@ -1,9 +1,141 @@
|
||||
//! `cph-typst` — typst `World`, driver generation, compile, PDF, span mapping.
|
||||
//!
|
||||
//! Owned by **WU-4**. Builds the typst compiler `World` over the engineering
|
||||
//! file's virtual file system (ADR-0006/0007), generates the driver entrypoint
|
||||
//! from the manifest, compiles to PDF, and maps typst spans back to source
|
||||
//! locations for diagnostics.
|
||||
//! Owned by **WU-4**. Embeds the typst 0.15 compiler as a library to:
|
||||
//! - **compile-check** a [`cph_model::Lesson`] (collect typst errors+warnings as
|
||||
//! [`cph_diag::Diagnostic`]s), and
|
||||
//! - **export a PDF** for a render target.
|
||||
//!
|
||||
//! The lesson's engineering-file directory tree (ADR-0007) is mounted as the
|
||||
//! typst project root; a generated *driver* entrypoint (one per target) lives
|
||||
//! in-memory under `.cph/driver-<target>.typ` and `include`s each part's
|
||||
//! content files (ADR-0006: a `content` value denotes the module **body**, so
|
||||
//! the driver uses `include`, never `import`). The driver hands an ordered
|
||||
//! `parts` array to the `@local/cph-render` package's `display` entry.
|
||||
//!
|
||||
//! ## typst version
|
||||
//!
|
||||
//! Pinned to the `typst` family `=0.15.0`. The 0.15 path/file API differs from
|
||||
//! 0.14: `FileId::new(RootedPath)` where `RootedPath::new(VirtualRoot,
|
||||
//! VirtualPath)`, and a diagnostic span is a `DiagSpan` (not a bare `Span`).
|
||||
|
||||
// WU-4: implement the World + driver-gen + compile + span mapping. Empty for
|
||||
// now so the workspace compiles.
|
||||
mod diag;
|
||||
mod driver;
|
||||
mod world;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cph_diag::Diagnostic;
|
||||
use cph_model::Lesson;
|
||||
use typst_kit::fonts::{self, FontStore};
|
||||
use typst_layout::PagedDocument;
|
||||
use typst_pdf::PdfOptions;
|
||||
|
||||
pub use driver::{generate_driver, DRIVER_DIR};
|
||||
pub use world::{render_package_spec, LessonWorld};
|
||||
|
||||
/// The compile/PDF engine: holds the shared font store and the on-disk location
|
||||
/// of the `cph-render` package.
|
||||
///
|
||||
/// Fonts are discovered once (system + embedded) and shared across every
|
||||
/// compilation via an [`std::sync::Arc`] inside the per-compile [`LessonWorld`].
|
||||
pub struct Engine {
|
||||
fonts: std::sync::Arc<FontStore>,
|
||||
render_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
/// Construct an engine, discovering system + embedded fonts. The render
|
||||
/// package directory is taken from the `CPH_RENDER_DIR` environment
|
||||
/// variable when set, otherwise defaults to `<repo>/render` resolved
|
||||
/// relative to this crate (`CARGO_MANIFEST_DIR/../../render`).
|
||||
pub fn new() -> Self {
|
||||
Self::with_render_dir(default_render_dir())
|
||||
}
|
||||
|
||||
/// Construct an engine pointing at an explicit `cph-render` package
|
||||
/// directory (the folder containing `lib.typ` / `typst.toml`).
|
||||
pub fn with_render_dir(render_dir: PathBuf) -> Self {
|
||||
let mut store = FontStore::new();
|
||||
// System fonts first (so the host's CJK fonts are preferred), then the
|
||||
// embedded fallbacks. `scan-fonts` / `embedded-fonts` features gate
|
||||
// these iterators.
|
||||
store.extend(fonts::system());
|
||||
store.extend(fonts::embedded());
|
||||
Self {
|
||||
fonts: std::sync::Arc::new(store),
|
||||
render_dir,
|
||||
}
|
||||
}
|
||||
|
||||
/// The directory this engine resolves `@local/cph-render` from.
|
||||
pub fn render_dir(&self) -> &std::path::Path {
|
||||
&self.render_dir
|
||||
}
|
||||
|
||||
/// Compile-check `lesson` for `target`, returning every typst diagnostic
|
||||
/// (errors **and** warnings) mapped to [`Diagnostic`]s. An empty `Vec` means
|
||||
/// a clean compile. Spans are resolved to files relative to `lesson.root`;
|
||||
/// spans landing in the generated driver are re-pointed with a hint marking
|
||||
/// them as driver-internal (a driver-gen bug indicator) rather than dropped.
|
||||
pub fn compile_check(&self, lesson: &Lesson, target: &str) -> Vec<Diagnostic> {
|
||||
let world = self.world_for(lesson, target);
|
||||
let warned = typst::compile::<PagedDocument>(&world);
|
||||
|
||||
let mut out = Vec::new();
|
||||
if let Err(errors) = &warned.output {
|
||||
out.extend(map_all(&world, errors));
|
||||
}
|
||||
out.extend(map_all(&world, &warned.warnings));
|
||||
out
|
||||
}
|
||||
|
||||
/// Build a PDF for `lesson` / `target`. On a clean compile returns the PDF
|
||||
/// bytes; on fatal errors returns the mapped diagnostics. Compile warnings
|
||||
/// are not surfaced here (use [`Engine::compile_check`] for those).
|
||||
pub fn build_pdf(&self, lesson: &Lesson, target: &str) -> Result<Vec<u8>, Vec<Diagnostic>> {
|
||||
let world = self.world_for(lesson, target);
|
||||
let warned = typst::compile::<PagedDocument>(&world);
|
||||
let doc = match warned.output {
|
||||
Ok(doc) => doc,
|
||||
Err(errors) => return Err(map_all(&world, &errors)),
|
||||
};
|
||||
typst_pdf::pdf(&doc, &PdfOptions::default()).map_err(|errors| map_all(&world, &errors))
|
||||
}
|
||||
|
||||
fn world_for(&self, lesson: &Lesson, target: &str) -> LessonWorld {
|
||||
let driver_src = generate_driver(lesson, target);
|
||||
LessonWorld::new(
|
||||
lesson.root.clone(),
|
||||
self.render_dir.clone(),
|
||||
target,
|
||||
driver_src,
|
||||
self.fonts.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Engine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a batch of typst diagnostics to `cph-diag` ones against `world`.
|
||||
fn map_all(world: &LessonWorld, diags: &[typst::diag::SourceDiagnostic]) -> Vec<Diagnostic> {
|
||||
diags
|
||||
.iter()
|
||||
.map(|d| diag::map_diagnostic(world, d))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `<crate>/../../render` — the repo's render package, used when `CPH_RENDER_DIR`
|
||||
/// is unset.
|
||||
fn default_render_dir() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("CPH_RENDER_DIR") {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("render")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user