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}