typst_bake_macros/
lib.rs

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