typst_count/world.rs
1//! A minimal implementation of Typst's World trait for document compilation.
2//!
3//! This module provides a simple world implementation that allows loading and
4//! compiling Typst documents from the filesystem. It handles file resolution,
5//! source loading, package resolution, and provides the minimal context needed for compilation.
6
7use anyhow::{Context, Result};
8use std::path::{Path, PathBuf};
9use typst::diag::{FileError, FileResult};
10use typst::foundations::{Bytes, Datetime};
11use typst::syntax::{FileId, Source, VirtualPath};
12use typst::text::{Font, FontBook};
13use typst::utils::LazyHash;
14use typst::{Library, LibraryExt, World};
15use typst_kit::download::{Downloader, ProgressSink};
16use typst_kit::fonts::{FontSlot, Fonts};
17use typst_kit::package::PackageStorage;
18
19/// A minimal implementation of Typst's `World` trait for standalone compilation.
20///
21/// This struct provides the bare minimum functionality needed to compile Typst
22/// documents. It handles file system access, source loading, package resolution,
23/// and maintains references to the Typst standard library.
24///
25/// # Limitations
26///
27/// - Uses a fixed date for compilation reproducibility
28/// - Resolves files relative to the main document's directory
29///
30/// # Examples
31///
32/// ```no_run
33/// use typst_count::world::SimpleWorld;
34/// use typst::World;
35/// use std::path::Path;
36///
37/// let world = SimpleWorld::new(Path::new("document.typ"))?;
38/// let main_id = world.main();
39/// # Ok::<(), anyhow::Error>(())
40/// ```
41pub struct SimpleWorld {
42 /// The Typst standard library
43 library: LazyHash<Library>,
44 /// Font book with discovered fonts
45 book: LazyHash<FontBook>,
46 /// Locations of and storage for lazily loaded fonts
47 fonts: Vec<FontSlot>,
48 /// File ID of the main document
49 main: FileId,
50 /// Root directory for resolving relative paths
51 root: PathBuf,
52 /// Package storage for @preview packages
53 package_storage: PackageStorage,
54}
55
56impl SimpleWorld {
57 /// Creates a new `SimpleWorld` for compiling a Typst document.
58 ///
59 /// This function initializes the compilation environment by:
60 /// 1. Canonicalizing the main file path
61 /// 2. Setting the root directory to the file's parent directory
62 /// 3. Creating a virtual path for the main file
63 /// 4. Initializing the Typst standard library
64 ///
65 /// # Arguments
66 ///
67 /// * `main_path` - Path to the main Typst document to compile
68 ///
69 /// # Returns
70 ///
71 /// A new `SimpleWorld` instance ready for compilation, or an error if
72 /// the file cannot be found or has no parent directory.
73 ///
74 /// # Errors
75 ///
76 /// Returns an error if:
77 /// - The file path cannot be canonicalized (file doesn't exist)
78 /// - The file has no parent directory
79 /// - The file has no filename component
80 ///
81 /// # Examples
82 ///
83 /// ```no_run
84 /// use typst_count::world::SimpleWorld;
85 /// use std::path::Path;
86 ///
87 /// let world = SimpleWorld::new(Path::new("document.typ"))?;
88 /// # Ok::<(), anyhow::Error>(())
89 /// ```
90 pub fn new(main_path: &Path) -> Result<Self> {
91 let main_path = main_path
92 .canonicalize()
93 .context("Failed to find input file")?;
94
95 let root = main_path
96 .parent()
97 .context("Input file has no parent directory")?
98 .to_path_buf();
99
100 let vpath = VirtualPath::new(
101 main_path
102 .file_name()
103 .context("Input file has no filename")?,
104 );
105 let main = FileId::new_fake(vpath);
106
107 // Initialize package storage with default cache and no custom paths
108 let downloader = Downloader::new("typst-count");
109 let package_storage = PackageStorage::new(None, None, downloader);
110
111 // Initialize fonts with system and embedded fonts
112 let mut font_searcher = Fonts::searcher();
113 font_searcher.include_system_fonts(true);
114 #[cfg(feature = "embed-fonts")]
115 font_searcher.include_embedded_fonts(true);
116 let fonts = font_searcher.search();
117
118 Ok(Self {
119 library: LazyHash::new(Library::builder().build()),
120 book: LazyHash::new(fonts.book),
121 fonts: fonts.fonts,
122 main,
123 root,
124 package_storage,
125 })
126 }
127
128 /// Resolves a file path for a given file ID.
129 ///
130 /// This handles both regular files (relative to root) and package files.
131 fn resolve_path(&self, id: FileId) -> FileResult<PathBuf> {
132 // Check if this is a package file
133 if let Some(spec) = id.package() {
134 // Prepare the package (download if needed, returns path to package dir)
135 let package_dir = self
136 .package_storage
137 .prepare_package(spec, &mut ProgressSink)
138 .map_err(|e| FileError::Other(Some(e.to_string().into())))?;
139
140 // Package files are stored in the package directory
141 // The vpath for package files includes the full path within the package
142 Ok(package_dir.join(id.vpath().as_rootless_path()))
143 } else {
144 // Regular file resolution
145 let path = if id.vpath().as_rootless_path().is_absolute() {
146 id.vpath().as_rootless_path().to_path_buf()
147 } else {
148 self.root.join(id.vpath().as_rootless_path())
149 };
150 Ok(path)
151 }
152 }
153}
154
155impl World for SimpleWorld {
156 /// Returns a reference to the Typst standard library.
157 fn library(&self) -> &LazyHash<Library> {
158 &self.library
159 }
160
161 /// Returns a reference to the font book.
162 fn book(&self) -> &LazyHash<FontBook> {
163 &self.book
164 }
165
166 /// Returns the file ID of the main document.
167 fn main(&self) -> FileId {
168 self.main
169 }
170
171 /// Loads the source code for a given file ID.
172 ///
173 /// This method resolves the file path (either absolute or relative to the
174 /// root directory) and reads the file contents as a UTF-8 string.
175 ///
176 /// # Arguments
177 ///
178 /// * `id` - The file ID to load
179 ///
180 /// # Returns
181 ///
182 /// A `Source` object containing the file's content and ID, or a file error
183 /// if the file cannot be read.
184 fn source(&self, id: FileId) -> FileResult<Source> {
185 let path = self.resolve_path(id)?;
186 let content = std::fs::read_to_string(&path).map_err(|e| FileError::from_io(e, &path))?;
187 Ok(Source::new(id, content))
188 }
189
190 /// Loads binary data for a given file ID.
191 ///
192 /// This method resolves the file path and reads the file contents as raw bytes.
193 /// Used for loading images, fonts, and other binary assets referenced by the document.
194 ///
195 /// # Arguments
196 ///
197 /// * `id` - The file ID to load
198 ///
199 /// # Returns
200 ///
201 /// A `Bytes` object containing the file's binary content, or a file error
202 /// if the file cannot be read.
203 fn file(&self, id: FileId) -> FileResult<Bytes> {
204 let path = self.resolve_path(id)?;
205 let content = std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))?;
206 Ok(Bytes::new(content))
207 }
208
209 /// Returns a font at the given index.
210 ///
211 /// Fonts are loaded lazily from the font book as needed by the compiler.
212 fn font(&self, index: usize) -> Option<Font> {
213 self.fonts.get(index)?.get()
214 }
215
216 /// Returns the current date for compilation.
217 ///
218 /// This implementation returns a fixed date (2024-01-01) for reproducibility.
219 /// The date is used by Typst's `datetime.today()` function but doesn't affect
220 /// word counting results.
221 ///
222 /// # Arguments
223 ///
224 /// * `_offset` - UTC offset in hours (ignored in this implementation)
225 fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
226 Some(Datetime::from_ymd(2024, 1, 1).unwrap())
227 }
228}