secunit_core/wisp/render/
typst.rs1use std::fs;
15
16use anyhow::{Context, Result};
17
18use super::{RenderRequest, RenderResult};
19
20#[cfg(not(feature = "pdf"))]
21pub fn render(req: &RenderRequest) -> Result<RenderResult> {
22 let typ_path = req.output.with_extension("typ");
23 if let Some(parent) = typ_path.parent() {
24 fs::create_dir_all(parent)
25 .with_context(|| format!("create output dir {}", parent.display()))?;
26 }
27 fs::write(&typ_path, req.typst_source)
28 .with_context(|| format!("write {}", typ_path.display()))?;
29 tracing::warn!(
30 "wisp export: built without the `pdf` feature — wrote Typst source to {} \
31 (no PDF compiled).",
32 typ_path.display()
33 );
34 Ok(RenderResult {
35 pages: None,
36 wrote_pdf: false,
37 intermediate: Some(typ_path),
38 })
39}
40
41#[cfg(feature = "pdf")]
42pub fn render(req: &RenderRequest) -> Result<RenderResult> {
43 use anyhow::{anyhow, bail};
44
45 if let Some(parent) = req.output.parent() {
46 fs::create_dir_all(parent)
47 .with_context(|| format!("create output dir {}", parent.display()))?;
48 }
49 let typ_path = req.output.with_extension("typ");
51 fs::write(&typ_path, req.typst_source)
52 .with_context(|| format!("write {}", typ_path.display()))?;
53
54 let world = world::WispWorld::new(req.template_dir, req.typst_source)?;
55
56 let typst::diag::Warned { output, warnings } =
57 typst::compile::<typst::layout::PagedDocument>(&world);
58 for w in &warnings {
59 tracing::warn!("typst: {}", w.message);
60 }
61 let document = output.map_err(|errs| {
62 let detail = errs
63 .iter()
64 .map(|e| e.message.to_string())
65 .collect::<Vec<_>>()
66 .join("; ");
67 anyhow!("typst compile failed: {detail}")
68 })?;
69
70 let pages = document.pages.len() as u32;
71
72 let pdf = typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
73 .map_err(|errs| anyhow!("typst PDF export failed: {} diagnostic(s)", errs.len()))?;
74 if pdf.is_empty() {
75 bail!("typst produced an empty PDF");
76 }
77 fs::write(req.output, &pdf).with_context(|| format!("write {}", req.output.display()))?;
78
79 Ok(RenderResult {
80 pages: Some(pages),
81 wrote_pdf: true,
82 intermediate: Some(typ_path),
83 })
84}
85
86#[cfg(feature = "pdf")]
87mod world {
88 use std::collections::HashMap;
89 use std::path::{Path, PathBuf};
90 use std::sync::Mutex;
91
92 use anyhow::{Context, Result};
93 use typst::diag::{FileError, FileResult};
94 use typst::foundations::{Bytes, Datetime};
95 use typst::syntax::{FileId, Source, VirtualPath};
96 use typst::text::{Font, FontBook};
97 use typst::utils::LazyHash;
98 use typst::{Library, World};
99
100 const FONTS: &[&[u8]] = &[
103 include_bytes!("../assets/fonts/Inter-Regular.ttf"),
104 include_bytes!("../assets/fonts/Inter-Medium.ttf"),
105 include_bytes!("../assets/fonts/Inter-SemiBold.ttf"),
106 include_bytes!("../assets/fonts/Inter-Bold.ttf"),
107 include_bytes!("../assets/fonts/Inter-Italic.ttf"),
108 include_bytes!("../assets/fonts/JetBrainsMono-Regular.ttf"),
109 include_bytes!("../assets/fonts/JetBrainsMono-Medium.ttf"),
110 ];
111
112 pub struct WispWorld {
113 library: LazyHash<Library>,
114 book: LazyHash<FontBook>,
115 fonts: Vec<Font>,
116 root: PathBuf,
117 main: FileId,
118 main_source: Source,
119 slots: Mutex<HashMap<FileId, FileResult<Bytes>>>,
121 }
122
123 impl WispWorld {
124 pub fn new(template_dir: &Path, main_src: &str) -> Result<Self> {
125 let root = template_dir
126 .canonicalize()
127 .with_context(|| format!("resolve template dir {}", template_dir.display()))?;
128
129 let mut fonts = Vec::new();
130 let mut book = FontBook::new();
131 for raw in FONTS {
132 let bytes = Bytes::new(*raw);
133 if let Some(font) = Font::new(bytes, 0) {
135 book.push(font.info().clone());
136 fonts.push(font);
137 }
138 }
139
140 let main = FileId::new(None, VirtualPath::new("main.typ"));
141 let main_source = Source::new(main, main_src.to_string());
142
143 Ok(Self {
144 library: LazyHash::new(Library::default()),
145 book: LazyHash::new(book),
146 fonts,
147 root,
148 main,
149 main_source,
150 slots: Mutex::new(HashMap::new()),
151 })
152 }
153
154 fn path_of(&self, id: FileId) -> FileResult<PathBuf> {
156 if id.package().is_some() {
158 return Err(FileError::NotFound(id.vpath().as_rootless_path().into()));
159 }
160 id.vpath()
161 .resolve(&self.root)
162 .ok_or_else(|| FileError::AccessDenied)
163 }
164
165 fn read_bytes(&self, id: FileId) -> FileResult<Bytes> {
166 if let Some(hit) = self.slots.lock().unwrap().get(&id) {
167 return hit.clone();
168 }
169 let result = (|| {
170 let path = self.path_of(id)?;
171 let data = std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))?;
172 Ok(Bytes::new(data))
173 })();
174 self.slots.lock().unwrap().insert(id, result.clone());
175 result
176 }
177 }
178
179 impl World for WispWorld {
180 fn library(&self) -> &LazyHash<Library> {
181 &self.library
182 }
183
184 fn book(&self) -> &LazyHash<FontBook> {
185 &self.book
186 }
187
188 fn main(&self) -> FileId {
189 self.main
190 }
191
192 fn source(&self, id: FileId) -> FileResult<Source> {
193 if id == self.main {
194 return Ok(self.main_source.clone());
195 }
196 let path = self.path_of(id)?;
199 let text = std::fs::read_to_string(&path).map_err(|e| FileError::from_io(e, &path))?;
200 Ok(Source::new(id, text))
201 }
202
203 fn file(&self, id: FileId) -> FileResult<Bytes> {
204 self.read_bytes(id)
205 }
206
207 fn font(&self, index: usize) -> Option<Font> {
208 self.fonts.get(index).cloned()
209 }
210
211 fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
212 None
214 }
215 }
216}