typst_bake_macros/
lib.rs

1//! Procedural macros for typst-bake
2//!
3//! This crate provides the `document!()` macro that embeds templates
4//! and packages at compile time.
5
6mod config;
7mod derive_intoval;
8mod dir_embed;
9mod downloader;
10mod scanner;
11
12use proc_macro::TokenStream;
13use quote::quote;
14use syn::{parse_macro_input, LitStr};
15
16/// Creates a Document with embedded templates, fonts, and packages.
17///
18/// # Usage
19///
20/// ```rust,ignore
21/// let pdf = typst_bake::document!("main.typ")
22///     .to_pdf()?;
23/// ```
24///
25/// # Configuration
26///
27/// Add to your Cargo.toml:
28/// ```toml
29/// [package.metadata.typst-bake]
30/// template-dir = "./templates"
31/// fonts-dir = "./fonts"
32/// ```
33#[proc_macro]
34pub fn document(input: TokenStream) -> TokenStream {
35    let entry = parse_macro_input!(input as LitStr);
36    let entry_value = entry.value();
37
38    // Get template directory
39    let template_dir = match config::get_template_dir() {
40        Ok(dir) => dir,
41        Err(e) => {
42            return syn::Error::new_spanned(entry, e)
43                .to_compile_error()
44                .into();
45        }
46    };
47
48    // Check if entry file exists
49    let entry_path = template_dir.join(&entry_value);
50    if !entry_path.exists() {
51        return syn::Error::new_spanned(
52            entry,
53            format!("Entry file not found: {}", entry_path.display()),
54        )
55        .to_compile_error()
56        .into();
57    }
58
59    // Get fonts directory
60    let fonts_dir = match config::get_fonts_dir() {
61        Ok(dir) => dir,
62        Err(e) => {
63            return syn::Error::new_spanned(entry, e)
64                .to_compile_error()
65                .into();
66        }
67    };
68
69    // Scan for packages
70    eprintln!("typst-bake: Scanning templates for package imports...");
71    let packages = scanner::extract_packages(&template_dir);
72
73    // Download packages if any
74    let cache_dir = match downloader::get_cache_dir() {
75        Ok(dir) => dir,
76        Err(e) => {
77            return syn::Error::new_spanned(entry, e)
78                .to_compile_error()
79                .into();
80        }
81    };
82
83    if !packages.is_empty() {
84        eprintln!(
85            "typst-bake: Found {} package(s) to bundle",
86            packages.len()
87        );
88
89        let refresh = config::should_refresh_cache();
90        if let Err(e) = downloader::download_packages(&packages, &cache_dir, refresh) {
91            return syn::Error::new_spanned(entry, e)
92                .to_compile_error()
93                .into();
94        }
95    } else {
96        eprintln!("typst-bake: No packages found in templates");
97    }
98
99    // Generate code
100    // We directly generate Dir struct code instead of using include_dir! macro
101    // This allows users to not need include_dir in their dependencies
102    let templates_result = dir_embed::embed_dir(&template_dir);
103    let fonts_result = dir_embed::embed_fonts_dir(&fonts_dir);
104
105    // Collect per-package stats and entries in a single pass
106    let mut package_infos = Vec::new();
107    let mut pkg_total_original = 0usize;
108    let mut pkg_total_compressed = 0usize;
109    let mut namespace_entries: Vec<proc_macro2::TokenStream> = Vec::new();
110
111    if cache_dir.exists() {
112        // Collect and sort namespaces
113        let mut ns_dirs: Vec<_> = std::fs::read_dir(&cache_dir)
114            .into_iter()
115            .flatten()
116            .filter_map(|e| e.ok())
117            .filter(|e| e.path().is_dir())
118            .collect();
119        ns_dirs.sort_by_key(|e| e.path());
120
121        for ns_entry in ns_dirs {
122            let ns_path = ns_entry.path();
123            let namespace = ns_path.file_name().unwrap().to_string_lossy().to_string();
124            let mut name_entries: Vec<proc_macro2::TokenStream> = Vec::new();
125
126            // Collect and sort names
127            let mut name_dirs: Vec<_> = std::fs::read_dir(&ns_path)
128                .into_iter()
129                .flatten()
130                .filter_map(|e| e.ok())
131                .filter(|e| e.path().is_dir())
132                .collect();
133            name_dirs.sort_by_key(|e| e.path());
134
135            for name_entry in name_dirs {
136                let name_path = name_entry.path();
137                let name = name_path.file_name().unwrap().to_string_lossy().to_string();
138                let mut version_entries: Vec<proc_macro2::TokenStream> = Vec::new();
139
140                // Collect and sort versions
141                let mut ver_dirs: Vec<_> = std::fs::read_dir(&name_path)
142                    .into_iter()
143                    .flatten()
144                    .filter_map(|e| e.ok())
145                    .filter(|e| e.path().is_dir())
146                    .collect();
147                ver_dirs.sort_by_key(|e| e.path());
148
149                for ver_entry in ver_dirs {
150                    let ver_path = ver_entry.path();
151                    let version = ver_path.file_name().unwrap().to_string_lossy().to_string();
152
153                    // Embed this package (only once!)
154                    let pkg_result = dir_embed::embed_dir(&ver_path);
155                    let pkg_name = format!("@{}/{}:{}", namespace, name, version);
156
157                    // Collect stats
158                    package_infos.push((
159                        pkg_name,
160                        pkg_result.original_size,
161                        pkg_result.compressed_size,
162                        pkg_result.file_count,
163                    ));
164                    pkg_total_original += pkg_result.original_size;
165                    pkg_total_compressed += pkg_result.compressed_size;
166
167                    // Build version directory entry
168                    let pkg_entries = &pkg_result.entries;
169                    version_entries.push(quote! {
170                        ::typst_bake::__internal::include_dir::DirEntry::Dir(
171                            ::typst_bake::__internal::include_dir::Dir::new(#version, &[#(#pkg_entries),*])
172                        )
173                    });
174                }
175
176                // Build name directory entry
177                name_entries.push(quote! {
178                    ::typst_bake::__internal::include_dir::DirEntry::Dir(
179                        ::typst_bake::__internal::include_dir::Dir::new(#name, &[#(#version_entries),*])
180                    )
181                });
182            }
183
184            // Build namespace directory entry
185            namespace_entries.push(quote! {
186                ::typst_bake::__internal::include_dir::DirEntry::Dir(
187                    ::typst_bake::__internal::include_dir::Dir::new(#namespace, &[#(#name_entries),*])
188                )
189            });
190        }
191    }
192
193    // Build final code
194    let templates_code = templates_result.to_dir_code("");
195    let fonts_code = fonts_result.to_dir_code("");
196    let packages_code = quote! {
197        ::typst_bake::__internal::include_dir::Dir::new("", &[#(#namespace_entries),*])
198    };
199
200    // Generate stats
201    let template_original = templates_result.original_size;
202    let template_compressed = templates_result.compressed_size;
203    let template_count = templates_result.file_count;
204
205    let font_original = fonts_result.original_size;
206    let font_compressed = fonts_result.compressed_size;
207    let font_count = fonts_result.file_count;
208
209    // Generate package info tokens
210    let pkg_info_tokens: Vec<_> = package_infos
211        .iter()
212        .map(|(name, orig, comp, count)| {
213            quote! {
214                ::typst_bake::PackageInfo {
215                    name: #name.to_string(),
216                    original_size: #orig,
217                    compressed_size: #comp,
218                    file_count: #count,
219                }
220            }
221        })
222        .collect();
223
224    let expanded = quote! {
225        {
226            use ::typst_bake::__internal::{Dir, Document};
227
228            static TEMPLATES: Dir<'static> = #templates_code;
229            static PACKAGES: Dir<'static> = #packages_code;
230            static FONTS: Dir<'static> = #fonts_code;
231
232            let stats = ::typst_bake::EmbedStats {
233                templates: ::typst_bake::CategoryStats {
234                    original_size: #template_original,
235                    compressed_size: #template_compressed,
236                    file_count: #template_count,
237                },
238                packages: ::typst_bake::PackageStats {
239                    packages: vec![#(#pkg_info_tokens),*],
240                    total_original: #pkg_total_original,
241                    total_compressed: #pkg_total_compressed,
242                },
243                fonts: ::typst_bake::CategoryStats {
244                    original_size: #font_original,
245                    compressed_size: #font_compressed,
246                    file_count: #font_count,
247                },
248            };
249
250            Document::__new(&TEMPLATES, &PACKAGES, &FONTS, #entry_value, stats)
251        }
252    };
253
254    expanded.into()
255}
256
257/// Derive macro for implementing `IntoValue` trait.
258///
259/// This allows structs to be converted to Typst values for use with `with_inputs()`.
260///
261/// # Example
262///
263/// ```rust,ignore
264/// use typst_bake::IntoValue;
265///
266/// #[derive(IntoValue)]
267/// struct Item {
268///     name: String,
269///     quantity: i32,
270/// }
271/// ```
272#[proc_macro_derive(IntoValue)]
273pub fn derive_into_value(item: TokenStream) -> TokenStream {
274    let item = parse_macro_input!(item as syn::DeriveInput);
275    derive_intoval::derive_into_value(item)
276        .unwrap_or_else(|err| err.to_compile_error())
277        .into()
278}
279
280/// Derive macro for adding `into_dict()` method to structs.
281///
282/// This allows structs to be converted to Typst Dict for use with `with_inputs()`.
283///
284/// # Example
285///
286/// ```rust,ignore
287/// use typst_bake::{IntoValue, IntoDict};
288///
289/// #[derive(IntoValue, IntoDict)]
290/// struct Data {
291///     title: String,
292///     count: i32,
293/// }
294///
295/// let pdf = typst_bake::document!("main.typ")
296///     .with_inputs(data.into_dict())
297///     .to_pdf()?;
298/// ```
299#[proc_macro_derive(IntoDict)]
300pub fn derive_into_dict(item: TokenStream) -> TokenStream {
301    let item = parse_macro_input!(item as syn::DeriveInput);
302    derive_intoval::derive_into_dict(item)
303        .unwrap_or_else(|err| err.to_compile_error())
304        .into()
305}