use std::fs;
use anyhow::{Context, Result};
use super::{RenderRequest, RenderResult};
#[cfg(not(feature = "pdf"))]
pub fn render(req: &RenderRequest) -> Result<RenderResult> {
let typ_path = req.output.with_extension("typ");
if let Some(parent) = typ_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create output dir {}", parent.display()))?;
}
fs::write(&typ_path, req.typst_source)
.with_context(|| format!("write {}", typ_path.display()))?;
tracing::warn!(
"wisp export: built without the `pdf` feature — wrote Typst source to {} \
(no PDF compiled).",
typ_path.display()
);
Ok(RenderResult {
pages: None,
wrote_pdf: false,
intermediate: Some(typ_path),
})
}
#[cfg(feature = "pdf")]
pub fn render(req: &RenderRequest) -> Result<RenderResult> {
use anyhow::{anyhow, bail};
if let Some(parent) = req.output.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create output dir {}", parent.display()))?;
}
let typ_path = req.output.with_extension("typ");
fs::write(&typ_path, req.typst_source)
.with_context(|| format!("write {}", typ_path.display()))?;
let world = world::WispWorld::new(req.template_dir, req.typst_source)?;
let typst::diag::Warned { output, warnings } =
typst::compile::<typst::layout::PagedDocument>(&world);
for w in &warnings {
tracing::warn!("typst: {}", w.message);
}
let document = output.map_err(|errs| {
let detail = errs
.iter()
.map(|e| e.message.to_string())
.collect::<Vec<_>>()
.join("; ");
anyhow!("typst compile failed: {detail}")
})?;
let pages = document.pages.len() as u32;
let pdf = typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
.map_err(|errs| anyhow!("typst PDF export failed: {} diagnostic(s)", errs.len()))?;
if pdf.is_empty() {
bail!("typst produced an empty PDF");
}
fs::write(req.output, &pdf).with_context(|| format!("write {}", req.output.display()))?;
Ok(RenderResult {
pages: Some(pages),
wrote_pdf: true,
intermediate: Some(typ_path),
})
}
#[cfg(feature = "pdf")]
mod world {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use anyhow::{Context, Result};
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, World};
const FONTS: &[&[u8]] = &[
include_bytes!("../assets/fonts/Inter-Regular.ttf"),
include_bytes!("../assets/fonts/Inter-Medium.ttf"),
include_bytes!("../assets/fonts/Inter-SemiBold.ttf"),
include_bytes!("../assets/fonts/Inter-Bold.ttf"),
include_bytes!("../assets/fonts/Inter-Italic.ttf"),
include_bytes!("../assets/fonts/JetBrainsMono-Regular.ttf"),
include_bytes!("../assets/fonts/JetBrainsMono-Medium.ttf"),
];
pub struct WispWorld {
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: Vec<Font>,
root: PathBuf,
main: FileId,
main_source: Source,
slots: Mutex<HashMap<FileId, FileResult<Bytes>>>,
}
impl WispWorld {
pub fn new(template_dir: &Path, main_src: &str) -> Result<Self> {
let root = template_dir
.canonicalize()
.with_context(|| format!("resolve template dir {}", template_dir.display()))?;
let mut fonts = Vec::new();
let mut book = FontBook::new();
for raw in FONTS {
let bytes = Bytes::new(*raw);
if let Some(font) = Font::new(bytes, 0) {
book.push(font.info().clone());
fonts.push(font);
}
}
let main = FileId::new(None, VirtualPath::new("main.typ"));
let main_source = Source::new(main, main_src.to_string());
Ok(Self {
library: LazyHash::new(Library::default()),
book: LazyHash::new(book),
fonts,
root,
main,
main_source,
slots: Mutex::new(HashMap::new()),
})
}
fn path_of(&self, id: FileId) -> FileResult<PathBuf> {
if id.package().is_some() {
return Err(FileError::NotFound(id.vpath().as_rootless_path().into()));
}
id.vpath()
.resolve(&self.root)
.ok_or_else(|| FileError::AccessDenied)
}
fn read_bytes(&self, id: FileId) -> FileResult<Bytes> {
if let Some(hit) = self.slots.lock().unwrap().get(&id) {
return hit.clone();
}
let result = (|| {
let path = self.path_of(id)?;
let data = std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))?;
Ok(Bytes::new(data))
})();
self.slots.lock().unwrap().insert(id, result.clone());
result
}
}
impl World for WispWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main {
return Ok(self.main_source.clone());
}
let path = self.path_of(id)?;
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.read_bytes(id)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index).cloned()
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
None
}
}
}