use std::{borrow::Cow, path::PathBuf};
use typst::{
diag::FileResult,
foundations::Bytes,
layout::{Abs, PagedDocument},
syntax::{FileId, Source},
};
use typst_as_lib::{
TypstEngine,
cached_file_resolver::{CachedFileResolver, IntoCachedFileResolver},
file_resolver::FileResolver,
package_resolver::{FileSystemCache, PackageResolver},
typst_kit_options::TypstKitFontOptions,
};
use crate::{PreviewError, Result};
const PACKAGE_CACHE_DIR: &str = "typst-package-cache";
#[derive(Debug, Default)]
pub struct TypstRenderOptions {
pub font_dirs: Vec<PathBuf>,
pub cache_dir: Option<PathBuf>,
}
enum PreviewPackageResolver {
FileSystem(CachedFileResolver<PackageResolver<FileSystemCache>>),
InMemory(CachedFileResolver<PackageResolver<typst_as_lib::package_resolver::InMemoryCache>>),
}
impl FileResolver for PreviewPackageResolver {
fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
match self {
Self::FileSystem(resolver) => resolver.resolve_binary(id),
Self::InMemory(resolver) => resolver.resolve_binary(id),
}
}
fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
match self {
Self::FileSystem(resolver) => resolver.resolve_source(id),
Self::InMemory(resolver) => resolver.resolve_source(id),
}
}
}
pub fn render_typst_svg(source: &str, options: TypstRenderOptions) -> Result<String> {
let search_options = TypstKitFontOptions::new()
.include_system_fonts(false)
.include_embedded_fonts(true)
.include_dirs(options.font_dirs);
let package_resolver = package_resolver(options.cache_dir)?;
let engine = TypstEngine::builder()
.main_file(source)
.search_fonts_with(search_options)
.add_file_resolver(package_resolver)
.build();
let document = engine
.compile::<PagedDocument>()
.output
.map_err(|error| PreviewError::Typst(error.to_string()))?;
Ok(normalize_typst_svg(typst_svg::svg_merged(&document, Abs::pt(0.0))))
}
fn package_resolver(cache_dir: Option<PathBuf>) -> Result<PreviewPackageResolver> {
match cache_dir {
Some(cache_dir) => {
let cache_dir = cache_dir.join(PACKAGE_CACHE_DIR);
std::fs::create_dir_all(&cache_dir)?;
Ok(PreviewPackageResolver::FileSystem(
PackageResolver::builder()
.cache(FileSystemCache(cache_dir))
.build()
.into_cached(),
))
}
None => Ok(PreviewPackageResolver::InMemory(
PackageResolver::builder().with_in_memory_cache().build().into_cached(),
)),
}
}
fn normalize_typst_svg(mut svg: String) -> String {
let marker = r##"<path class="typst-shape""##;
let mut cursor = 0;
while let Some(relative_idx) = svg[cursor..].find(marker) {
let idx = cursor + relative_idx;
let Some(relative_end) = svg[idx..].find("/>") else {
break;
};
let end = idx + relative_end + 2;
let fragment = &svg[idx..end];
if fragment.contains(r#"d="M 0 0v "#) && fragment.contains(r#" h "#) && fragment.contains(r#" v -"#) {
svg.replace_range(idx..end, "");
cursor = idx;
} else {
cursor = end;
}
}
svg
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_without_page_background() {
let svg = render_typst_svg("= Preview", TypstRenderOptions::default()).unwrap();
assert!(svg.starts_with("<svg"));
assert!(!svg.contains(r#"d="M 0 0v "#));
}
}