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