karnaugh/src/typst.rs
2026-07-19 15:20:38 +02:00

204 lines
6.4 KiB
Rust

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<Library>,
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<String, Vec<SourceDiagnostic>> {
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<Library> {
&self.context.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.context.fonts.book()
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
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<Bytes> {
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<Font> {
self.context.fonts.font(index)
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
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<PathBuf> {
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<PathBuf> {
path.realize(&self.context.config.content_root)
.map_err(FileError::Realize)
}
fn resolve_package_file(&self, path: &VirtualPath, spec: &PackageSpec) -> FileResult<PathBuf> {
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<PathBuf> {
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<PathBuf> {
let package_path = self.get_package(spec)?;
path.realize(&package_path).map_err(FileError::Realize)
}
fn get_package(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
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.")
}