forked from EduCraft/curriculum-project-hub
d76f9f9a54
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>
121 lines
4.6 KiB
Rust
121 lines
4.6 KiB
Rust
//! Map typst [`SourceDiagnostic`]s to [`cph_diag::Diagnostic`]s.
|
|
//!
|
|
//! ## Span mapping (typst 0.15 specifics)
|
|
//!
|
|
//! In 0.15 a diagnostic's `span` is a [`typst::syntax::DiagSpan`] (not the bare
|
|
//! `Span` of 0.14). To locate it we:
|
|
//! 1. `span.id() -> Option<FileId>` for the owning file (`None` ⇒ detached, so
|
|
//! no span is emitted);
|
|
//! 2. `WorldExt::range(span) -> Option<Range<usize>>` for the byte range — this
|
|
//! handles every `DiagSpanKind` variant internally (numbered source spans
|
|
//! *and* raw external byte ranges), so we never match the enum by hand;
|
|
//! 3. compute 1-based line/col from the file's [`typst::syntax::Source`] via
|
|
//! `lines().byte_to_line_column` (0-based ⇒ `+1`).
|
|
//!
|
|
//! The `main` file is now the real **template** (`exports/<target>.typ`), so a
|
|
//! span there resolves naturally to an authored file — no re-pointing needed. A
|
|
//! span landing in the **virtual augmented manifest** ([`crate::MANIFEST_VPATH`],
|
|
//! served in-memory, not on disk) is reported with its vpath plus a hint marking
|
|
//! it as the framework-synthesized manifest (a manifest-generation bug
|
|
//! indicator) rather than mis-attributed to a user file. Spans in the
|
|
//! `cph-render` package are reported with an `@local/cph-render:…` pseudo-path.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use cph_diag::{DiagCode, Diagnostic, Severity, SourceSpan};
|
|
use typst::diag::{Severity as TypstSeverity, SourceDiagnostic};
|
|
use typst::syntax::{FileId, VirtualRoot};
|
|
use typst::{World, WorldExt};
|
|
|
|
use crate::world::LessonWorld;
|
|
|
|
/// Convert one typst diagnostic into a `cph-diag` [`Diagnostic`], resolving its
|
|
/// span against `world`.
|
|
pub fn map_diagnostic(world: &LessonWorld, d: &SourceDiagnostic) -> Diagnostic {
|
|
let severity = match d.severity {
|
|
TypstSeverity::Error => Severity::Error,
|
|
TypstSeverity::Warning => Severity::Warning,
|
|
};
|
|
|
|
// Flatten the typst call trace into the message tail so call-site context
|
|
// isn't lost (the primary span points at the innermost node).
|
|
let mut message = d.message.to_string();
|
|
for tp in &d.trace {
|
|
message.push_str(&format!("\n in {}", tp.v));
|
|
}
|
|
|
|
let mut diag = Diagnostic {
|
|
severity,
|
|
code: DiagCode::TypstCompile,
|
|
message,
|
|
span: None,
|
|
hint: None,
|
|
};
|
|
|
|
// typst's own hints are the product's core value — carry them through.
|
|
let mut hints: Vec<String> = d.hints.iter().map(|h| h.v.to_string()).collect();
|
|
|
|
if let Some(id) = d.span.id() {
|
|
if let Some(file) = file_for(world, id) {
|
|
if is_virtual_manifest(id) {
|
|
hints.push(
|
|
"this span is inside the framework-synthesized manifest \
|
|
(/.cph/manifest.toml), not a lesson source file — likely a \
|
|
manifest-generation issue"
|
|
.to_string(),
|
|
);
|
|
}
|
|
// Byte range via WorldExt::range (DiagSpan is Copy).
|
|
let byte_range = world.range(d.span).unwrap_or(0..0);
|
|
let (line, col) = line_col(world, id, byte_range.start);
|
|
diag.span = Some(SourceSpan {
|
|
file,
|
|
byte_range,
|
|
line,
|
|
col,
|
|
});
|
|
}
|
|
}
|
|
|
|
if !hints.is_empty() {
|
|
diag.hint = Some(hints.join("; "));
|
|
}
|
|
diag
|
|
}
|
|
|
|
/// Whether `id` is the in-memory augmented-manifest virtual file
|
|
/// ([`crate::MANIFEST_VPATH`]) the engine injects (not a real lesson file).
|
|
fn is_virtual_manifest(id: FileId) -> bool {
|
|
matches!(id.root(), VirtualRoot::Project)
|
|
&& id.vpath().get_with_slash() == crate::MANIFEST_VPATH
|
|
}
|
|
|
|
/// 1-based (line, col) for a byte offset in file `id`, defaulting to `(1, 1)`
|
|
/// when the source can't be read or the offset is out of range.
|
|
fn line_col(world: &LessonWorld, id: FileId, byte: usize) -> (u32, u32) {
|
|
let Ok(source) = world.source(id) else {
|
|
return (1, 1);
|
|
};
|
|
match source.lines().byte_to_line_column(byte) {
|
|
// byte_to_line_column is 0-based; editors are 1-based.
|
|
Some((line, col)) => (line as u32 + 1, col as u32 + 1),
|
|
None => (1, 1),
|
|
}
|
|
}
|
|
|
|
/// File path for a `FileId`. Project files are relative to the lesson root (no
|
|
/// leading slash); package files use an `@namespace/name:version/path` form.
|
|
fn file_for(world: &LessonWorld, id: FileId) -> Option<PathBuf> {
|
|
let _ = world;
|
|
match id.root() {
|
|
VirtualRoot::Project => Some(PathBuf::from(id.vpath().get_without_slash())),
|
|
VirtualRoot::Package(spec) => Some(PathBuf::from(format!(
|
|
"@{}/{}:{}{}",
|
|
spec.namespace,
|
|
spec.name,
|
|
spec.version,
|
|
id.vpath().get_with_slash()
|
|
))),
|
|
}
|
|
}
|