use std::path::PathBuf; use time::{OffsetDateTime, PrimitiveDateTime, UtcDateTime, UtcOffset}; use tracing::info; use typst::{ Library, LibraryExt, World, diag::{FileError, FileResult, PackageError, PackageResult, SourceDiagnostic}, foundations::{Bytes, Datetime, Dict, Duration, Value}, syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot, package::PackageSpec}, text::{Font, FontBook}, utils::LazyHash, }; use typst_html::HtmlOptions; use typst_kit::fonts::FontStore; use crate::AppConfig; pub struct TypstContext { config: AppConfig, library: LazyHash, fonts: FontStore, } impl TypstContext { pub fn new(config: AppConfig) -> Self { let mut inputs = Dict::new(); if config.syntax_root.is_some() { inputs.insert( "highlight".into(), Value::Func(crate::syntax::highlight_func()), ); } let library = Library::builder() .with_inputs(inputs) .with_features([typst::Feature::Html].into_iter().collect()) .build(); let library = LazyHash::new(library); let fonts = FontStore::new(); Self { config, library, fonts, } } // TODO: Better return error pub fn compile_document(&self, path: VirtualPath) -> Result> { info!("Compiling {:?}", &path); let world = DocumentWorld::new(path, self); let compiled = typst::compile(&world); let html_options = HtmlOptions::default(); // TODO: Log warnings // TODO: Log errors compiled .output .and_then(|doc| typst_html::html(&doc, &html_options)) .map_err(|err| err.into_iter().collect()) } } struct DocumentWorld<'a> { main: FileId, now: UtcDateTime, context: &'a TypstContext, } impl<'a> World for DocumentWorld<'a> { fn library(&self) -> &LazyHash { &self.context.library } fn book(&self) -> &LazyHash { self.context.fonts.book() } fn main(&self) -> FileId { self.main } fn source(&self, id: FileId) -> FileResult { self.resolve_file(id).and_then(|path| { let text = std::fs::read_to_string(&path).map_err(|e| FileError::from_io(e, &path))?; Ok(Source::new(id, text)) }) } fn file(&self, id: FileId) -> FileResult { self.resolve_file(id).and_then(|path| { let bytes = std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))?; Ok(Bytes::new(bytes)) }) } fn font(&self, index: usize) -> Option { self.context.fonts.font(index) } fn today(&self, offset: Option) -> Option { let offset = offset .map(|v| v.seconds() as i32) .and_then(|v| UtcOffset::from_whole_seconds(v).ok()) .unwrap_or(UtcOffset::UTC); let datetime = OffsetDateTime::from(self.now).replace_offset(offset); let datetime = PrimitiveDateTime::new(datetime.date(), datetime.time()); Some(Datetime::Datetime(datetime)) } } impl<'a> DocumentWorld<'a> { fn new(path: VirtualPath, context: &'a TypstContext) -> Self { let main = FileId::new(RootedPath::new(VirtualRoot::Project, path)); let now = UtcDateTime::now(); Self { main, now, context } } fn resolve_file(&self, id: FileId) -> FileResult { let path = id.vpath(); match id.root() { VirtualRoot::Project => self.resolve_project_file(path), VirtualRoot::Package(package_spec) => self.resolve_package_file(path, package_spec), } } fn resolve_project_file(&self, path: &VirtualPath) -> FileResult { path.realize(&self.context.config.content_root) .map_err(FileError::Realize) } fn resolve_package_file(&self, path: &VirtualPath, spec: &PackageSpec) -> FileResult { match spec.namespace.as_str() { "common" => self.resolve_common_file(path, spec), "preview" => self.resolve_preview_file(path, spec), _ => unimplemented!(), } } fn resolve_common_file(&self, path: &VirtualPath, spec: &PackageSpec) -> FileResult { let path = get_package_path(path, spec); path.realize(&self.context.config.get_full_common_path()) .map_err(FileError::Realize) } fn resolve_preview_file(&self, path: &VirtualPath, spec: &PackageSpec) -> FileResult { let package_path = self.get_package(spec)?; path.realize(&package_path).map_err(FileError::Realize) } fn get_package(&self, spec: &PackageSpec) -> PackageResult { let path = get_package_path( &VirtualPath::new("").expect("Failed to create empty virtual path"), spec, ); let path = path .realize(&self.context.config.packages_root) .map_err(|_| PackageError::NotFound(spec.clone()))?; if path.exists() { return Ok(path); } let url = format!( "https://packages.typst.org/preview/{}-{}.tar.gz", spec.name, spec.version ); info!("Downloading package from {}", url); let response = ureq::get(&url) .call() .map_err(|err| err.to_string()) .and_then(|res| { if res.status().is_success() { Ok(res) } else { Err(format!("Received status code {}", res.status())) } }) .map_err(|err| PackageError::NetworkFailed(Some(err.into())))?; let archive = response.into_body().into_reader(); let archive = flate2::read::GzDecoder::new(archive); let mut archive = tar::Archive::new(archive); archive.unpack(&path).map_err(|err| { // let _ = std::fs::remove_dir_all(path.clone()); PackageError::MalformedArchive(Some(err.to_string().into())) })?; Ok(path) } } fn get_package_path(file_path: &VirtualPath, spec: &PackageSpec) -> VirtualPath { let package_path = VirtualPath::new(format!("{}/{}", spec.name, spec.version)) .expect("Failed to create virtual path representing package path."); package_path .join(file_path.get_without_slash()) .expect("Failed to join file path into package path.") }