204 lines
6.4 KiB
Rust
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.")
|
|
}
|