1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, OnceLock, RwLock};
6
7use typst::diag::{FileError, FileResult};
8use typst::foundations::{Bytes, Datetime, Dict, Duration};
9use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
10use typst::text::{Font, FontBook};
11use typst::utils::LazyHash;
12use typst::{Library, LibraryExt, World};
13
14use crate::Fonts;
15
16pub fn project_file(path: VirtualPath) -> FileId {
18 RootedPath::new(VirtualRoot::Project, path).intern()
19}
20
21struct FileSlot {
23 source: OnceLock<FileResult<Source>>,
24 bytes: OnceLock<FileResult<Bytes>>,
25}
26
27impl FileSlot {
28 fn new() -> Self {
29 Self {
30 source: OnceLock::new(),
31 bytes: OnceLock::new(),
32 }
33 }
34}
35
36#[cfg(not(target_arch = "wasm32"))]
38pub struct Dependency {
39 pub name: String,
42 pub path: PathBuf,
44 pub data: Bytes,
46}
47
48pub struct EmbeddedWorld {
55 main: Source,
56 library: LazyHash<Library>,
57 fonts: Arc<Fonts>,
58 files: RwLock<HashMap<FileId, FileSlot>>,
59 roots: Vec<PathBuf>,
60 #[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
61 packages: Option<crate::Packages>,
62 fixed_now: Option<chrono::DateTime<chrono::Utc>>,
63 #[cfg(not(target_arch = "wasm32"))]
64 now: OnceLock<chrono::DateTime<chrono::Utc>>,
65}
66
67impl EmbeddedWorld {
68 pub fn new(main: VirtualPath, source: impl Into<String>, fonts: Arc<Fonts>) -> Self {
71 Self {
72 main: Source::new(project_file(main), source.into()),
73 library: LazyHash::new(Library::default()),
74 fonts,
75 files: RwLock::new(HashMap::new()),
76 roots: Vec::new(),
77 #[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
78 packages: None,
79 fixed_now: None,
80 #[cfg(not(target_arch = "wasm32"))]
81 now: OnceLock::new(),
82 }
83 }
84
85 pub fn with_roots(mut self, roots: Vec<PathBuf>) -> Self {
88 self.roots = roots;
89 self
90 }
91
92 pub fn with_inputs(mut self, inputs: Dict) -> Self {
94 self.library = LazyHash::new(Library::builder().with_inputs(inputs).build());
95 self
96 }
97
98 pub fn with_creation_timestamp(mut self, seconds: i64) -> Self {
102 self.fixed_now = chrono::DateTime::from_timestamp(seconds, 0);
103 self
104 }
105
106 #[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
108 pub fn with_packages(mut self, packages: crate::Packages) -> Self {
109 self.packages = Some(packages);
110 self
111 }
112
113 pub fn add_file(&self, path: VirtualPath, contents: impl AsRef<[u8]> + Send + Sync + 'static) {
115 let mut files = self
116 .files
117 .write()
118 .expect("virtual filesystem lock poisoned");
119 let slot = files
120 .entry(project_file(path))
121 .or_insert_with(FileSlot::new);
122 let _ = slot.bytes.set(Ok(Bytes::new(contents)));
123 }
124
125 pub fn fonts(&self) -> &Fonts {
127 &self.fonts
128 }
129
130 pub fn path_for(&self, id: FileId) -> FileResult<PathBuf> {
133 let not_found = || FileError::NotFound(id.vpath().get_with_slash().into());
134 match id.root() {
135 VirtualRoot::Project => self
136 .roots
137 .iter()
138 .filter_map(|root| id.vpath().realize(root).ok())
139 .find(|path| path.is_file())
140 .ok_or_else(not_found),
141 #[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
142 VirtualRoot::Package(spec) => {
143 let packages = self.packages.as_ref().ok_or_else(not_found)?;
144 let root = packages.root(spec)?;
145 id.vpath().realize(&root).map_err(|_| not_found())
146 }
147 #[cfg(not(all(feature = "packages", not(target_arch = "wasm32"))))]
148 VirtualRoot::Package(_) => Err(not_found()),
149 }
150 }
151
152 pub fn display_path(&self, id: FileId) -> String {
155 let Ok(path) = self.path_for(id) else {
156 return id.vpath().get_without_slash().to_owned();
157 };
158 std::env::current_dir()
159 .ok()
160 .and_then(|cwd| cwd.canonicalize().ok())
161 .and_then(|cwd| path.strip_prefix(cwd).ok().map(Path::to_path_buf))
162 .unwrap_or(path)
163 .display()
164 .to_string()
165 }
166
167 #[cfg(not(target_arch = "wasm32"))]
170 pub fn dependencies(&self) -> Vec<Dependency> {
171 let files = self.files.read().expect("virtual filesystem lock poisoned");
172 let mut dependencies = Vec::new();
173 for (id, slot) in files.iter() {
174 let data = match (slot.bytes.get(), slot.source.get()) {
175 (Some(Ok(bytes)), _) => bytes.clone(),
176 (_, Some(Ok(source))) => Bytes::from_string(source.text().to_owned()),
177 _ => continue,
178 };
179 let Ok(path) = self.path_for(*id) else {
180 continue;
181 };
182 let name = match id.root() {
183 VirtualRoot::Project => id.vpath().get_without_slash().to_owned(),
184 VirtualRoot::Package(spec) => format!("{spec}/{}", id.vpath().get_without_slash()),
185 };
186 dependencies.push(Dependency { name, path, data });
187 }
188 dependencies
189 }
190
191 fn cached_or_load<T: Clone>(
192 &self,
193 id: FileId,
194 cell: impl Fn(&FileSlot) -> &OnceLock<FileResult<T>>,
195 load: impl FnOnce(&Path) -> FileResult<T>,
196 ) -> FileResult<T> {
197 {
198 let files = self.files.read().expect("virtual filesystem lock poisoned");
199 if let Some(result) = files.get(&id).and_then(|slot| cell(slot).get()) {
200 return result.clone();
201 }
202 }
203
204 let result = self.path_for(id).and_then(|path| load(&path));
205
206 let mut files = self
207 .files
208 .write()
209 .expect("virtual filesystem lock poisoned");
210 let slot = files.entry(id).or_insert_with(FileSlot::new);
211 cell(slot).get_or_init(|| result.clone()).clone()
212 }
213}
214
215impl World for EmbeddedWorld {
216 fn library(&self) -> &LazyHash<Library> {
217 &self.library
218 }
219
220 fn book(&self) -> &LazyHash<FontBook> {
221 self.fonts.book()
222 }
223
224 fn main(&self) -> FileId {
225 self.main.id()
226 }
227
228 fn source(&self, id: FileId) -> FileResult<Source> {
229 if id == self.main.id() {
230 return Ok(self.main.clone());
231 }
232
233 {
234 let files = self.files.read().expect("virtual filesystem lock poisoned");
235 if let Some(Ok(bytes)) = files.get(&id).and_then(|slot| slot.bytes.get()) {
236 let text =
237 std::str::from_utf8(bytes.as_slice()).map_err(|_| FileError::InvalidUtf8)?;
238 return Ok(Source::new(id, text.to_owned()));
239 }
240 }
241
242 self.cached_or_load(
243 id,
244 |slot| &slot.source,
245 |path| {
246 std::fs::read_to_string(path)
247 .map_err(|error| FileError::from_io(error, path))
248 .map(|text| Source::new(id, text))
249 },
250 )
251 }
252
253 fn file(&self, id: FileId) -> FileResult<Bytes> {
254 self.cached_or_load(
255 id,
256 |slot| &slot.bytes,
257 |path| {
258 std::fs::read(path)
259 .map_err(|error| FileError::from_io(error, path))
260 .map(Bytes::new)
261 },
262 )
263 }
264
265 fn font(&self, index: usize) -> Option<Font> {
266 self.fonts.font(index)
267 }
268
269 fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
270 use chrono::Datelike;
271
272 let (now, local) = match self.fixed_now {
276 Some(now) => (now, false),
277 #[cfg(not(target_arch = "wasm32"))]
278 None => (*self.now.get_or_init(chrono::Utc::now), true),
279 #[cfg(target_arch = "wasm32")]
280 None => return None,
281 };
282 let date = match offset {
283 #[cfg(not(target_arch = "wasm32"))]
284 None if local => now.with_timezone(&chrono::Local).date_naive(),
285 None => now.date_naive(),
286 Some(offset) => {
287 (now + chrono::Duration::try_seconds(offset.seconds() as i64)?).date_naive()
288 }
289 };
290 #[cfg(target_arch = "wasm32")]
291 let _ = local;
292 Datetime::from_ymd(
293 date.year(),
294 date.month().try_into().ok()?,
295 date.day().try_into().ok()?,
296 )
297 }
298}