1mod compression_cache;
8mod config;
9mod derive_intoval;
10mod dir_embed;
11mod downloader;
12mod scanner;
13
14use std::collections::{BTreeMap, BTreeSet};
15use std::path::{Path, PathBuf};
16
17use proc_macro::TokenStream;
18use quote::quote;
19use syn::{parse_macro_input, LitStr};
20
21use compression_cache::CompressionCache;
22use dir_embed::DirEmbedResult;
23
24use scanner::PackageSpec;
25
26#[derive(Debug)]
28struct MacroPackageInfo {
29 name: String,
30 original_size: usize,
31 compressed_size: usize,
32 file_count: usize,
33}
34
35type ResolvedPackages = (Vec<PackageSpec>, PathBuf);
37
38struct EmbeddedPackages {
40 infos: Vec<MacroPackageInfo>,
41 total_original: usize,
42 total_compressed: usize,
43 namespace_entries: Vec<proc_macro2::TokenStream>,
44}
45
46fn resolve_config(
48 entry: &LitStr,
49 entry_value: &str,
50) -> Result<(PathBuf, PathBuf), proc_macro2::TokenStream> {
51 let template_dir = config::get_template_dir()
52 .map_err(|e| syn::Error::new_spanned(entry, e).to_compile_error())?;
53
54 let entry_path = template_dir.join(entry_value);
55 if !entry_path.exists() {
56 return Err(syn::Error::new_spanned(
57 entry,
58 format!("Entry file not found: {}", entry_path.display()),
59 )
60 .to_compile_error());
61 }
62
63 let fonts_dir = config::get_fonts_dir()
64 .map_err(|e| syn::Error::new_spanned(entry, e).to_compile_error())?;
65
66 Ok((template_dir, fonts_dir))
67}
68
69fn resolve_and_download_packages(
71 entry: &LitStr,
72 template_dir: &Path,
73) -> Result<ResolvedPackages, proc_macro2::TokenStream> {
74 eprintln!("typst-bake: Scanning for package imports...");
75 let packages = scanner::extract_packages(template_dir);
76
77 let cache_dir = downloader::get_cache_dir()
78 .map_err(|e| syn::Error::new_spanned(entry, e).to_compile_error())?;
79
80 let resolved_packages = if !packages.is_empty() {
81 eprintln!("typst-bake: Found {} package(s) to bundle", packages.len());
82
83 let refresh = config::should_refresh_cache();
84 downloader::download_packages(&packages, &cache_dir, refresh)
85 .map_err(|e| syn::Error::new_spanned(entry, e).to_compile_error())?
86 } else {
87 eprintln!("typst-bake: No packages found");
88 Vec::new()
89 };
90
91 Ok((resolved_packages, cache_dir))
92}
93
94fn dir_entry_token(name: &str, children: &[proc_macro2::TokenStream]) -> proc_macro2::TokenStream {
96 quote! {
97 ::typst_bake::__internal::include_dir::DirEntry::Dir(
98 ::typst_bake::__internal::include_dir::Dir::new(#name, &[#(#children),*])
99 )
100 }
101}
102
103fn embed_packages(
105 resolved_packages: &[PackageSpec],
106 cache_dir: &Path,
107 cache: &mut CompressionCache,
108) -> EmbeddedPackages {
109 let mut package_infos = Vec::new();
110 let mut pkg_total_original = 0;
111 let mut pkg_total_compressed = 0;
112 let mut namespace_entries = Vec::new();
113
114 let mut pkg_tree: BTreeMap<&str, BTreeMap<&str, BTreeSet<&str>>> = BTreeMap::new();
116 for pkg in resolved_packages {
117 pkg_tree
118 .entry(pkg.namespace.as_str())
119 .or_default()
120 .entry(pkg.name.as_str())
121 .or_default()
122 .insert(pkg.version.as_str());
123 }
124
125 for (namespace, names) in &pkg_tree {
126 let mut name_entries = Vec::new();
127
128 for (name, versions) in names {
129 let mut version_entries = Vec::new();
130
131 for version in versions {
132 let ver_path = cache_dir.join(namespace).join(name).join(version);
133
134 let pkg_result = dir_embed::embed_dir(&ver_path, cache);
135 let pkg_name = format!("@{namespace}/{name}:{version}");
136
137 package_infos.push(MacroPackageInfo {
138 name: pkg_name,
139 original_size: pkg_result.original_size,
140 compressed_size: pkg_result.compressed_size,
141 file_count: pkg_result.file_count,
142 });
143 pkg_total_original += pkg_result.original_size;
144 pkg_total_compressed += pkg_result.compressed_size;
145
146 version_entries.push(dir_entry_token(version, &pkg_result.entries));
147 }
148
149 name_entries.push(dir_entry_token(name, &version_entries));
150 }
151
152 namespace_entries.push(dir_entry_token(namespace, &name_entries));
153 }
154
155 EmbeddedPackages {
156 infos: package_infos,
157 total_original: pkg_total_original,
158 total_compressed: pkg_total_compressed,
159 namespace_entries,
160 }
161}
162
163fn generate_output(
165 entry_value: &str,
166 templates_result: &DirEmbedResult,
167 fonts_result: &DirEmbedResult,
168 packages: &EmbeddedPackages,
169 cache: &mut CompressionCache,
170 compression_level: i32,
171) -> proc_macro2::TokenStream {
172 cache.log_summary();
173 cache.cleanup();
174
175 let dedup = cache.dedup_summary();
176 let dedup_total_files = dedup.total_files;
177 let dedup_unique_blobs = dedup.unique_blobs;
178 let dedup_duplicate_count = dedup.duplicate_count;
179 let dedup_saved_bytes = dedup.saved_bytes;
180 let dedup_statics = cache.dedup_statics();
181
182 let templates_code = templates_result.to_dir_code("");
183 let fonts_code = fonts_result.to_dir_code("");
184 let namespace_entries = &packages.namespace_entries;
185 let packages_code = quote! {
186 ::typst_bake::__internal::include_dir::Dir::new("", &[#(#namespace_entries),*])
187 };
188
189 let template_original = templates_result.original_size;
190 let template_compressed = templates_result.compressed_size;
191 let template_count = templates_result.file_count;
192
193 let font_original = fonts_result.original_size;
194 let font_compressed = fonts_result.compressed_size;
195 let font_count = fonts_result.file_count;
196
197 let pkg_total_original = packages.total_original;
198 let pkg_total_compressed = packages.total_compressed;
199
200 let pkg_info_tokens: Vec<_> = packages
201 .infos
202 .iter()
203 .map(|info| {
204 let name = &info.name;
205 let orig = info.original_size;
206 let comp = info.compressed_size;
207 let count = info.file_count;
208 quote! {
209 ::typst_bake::PackageInfo {
210 name: #name.to_string(),
211 original_size: #orig,
212 compressed_size: #comp,
213 file_count: #count,
214 }
215 }
216 })
217 .collect();
218
219 quote! {
220 {
221 use ::typst_bake::__internal::{Dir, Document};
222
223 #(#dedup_statics)*
224
225 static TEMPLATES: Dir<'static> = #templates_code;
226 static PACKAGES: Dir<'static> = #packages_code;
227 static FONTS: Dir<'static> = #fonts_code;
228
229 let stats = ::typst_bake::EmbedStats {
230 templates: ::typst_bake::CategoryStats {
231 original_size: #template_original,
232 compressed_size: #template_compressed,
233 file_count: #template_count,
234 },
235 packages: ::typst_bake::PackageStats {
236 packages: vec![#(#pkg_info_tokens),*],
237 original_size: #pkg_total_original,
238 compressed_size: #pkg_total_compressed,
239 },
240 fonts: ::typst_bake::CategoryStats {
241 original_size: #font_original,
242 compressed_size: #font_compressed,
243 file_count: #font_count,
244 },
245 dedup: ::typst_bake::DedupStats {
246 total_files: #dedup_total_files,
247 unique_blobs: #dedup_unique_blobs,
248 duplicate_count: #dedup_duplicate_count,
249 saved_bytes: #dedup_saved_bytes,
250 },
251 compression_level: #compression_level,
252 };
253
254 Document::__new(&TEMPLATES, &PACKAGES, &FONTS, #entry_value, stats)
255 }
256 }
257}
258
259#[proc_macro]
260pub fn document(input: TokenStream) -> TokenStream {
261 let entry = parse_macro_input!(input as LitStr);
262 let entry_value = entry.value();
263
264 let (template_dir, fonts_dir) = match resolve_config(&entry, &entry_value) {
265 Ok(v) => v,
266 Err(e) => return e.into(),
267 };
268
269 let (resolved_packages, cache_dir) = match resolve_and_download_packages(&entry, &template_dir)
270 {
271 Ok(v) => v,
272 Err(e) => return e.into(),
273 };
274
275 let compression_level = config::get_compression_level();
276 let compression_cache_dir = config::get_compression_cache_dir()
277 .map_err(|e| eprintln!("typst-bake: Compression cache disabled: {e}"))
278 .ok();
279 let mut cache = CompressionCache::new(compression_cache_dir, compression_level);
280
281 let templates_result = dir_embed::embed_dir(&template_dir, &mut cache);
282 let fonts_result = dir_embed::embed_fonts_dir(&fonts_dir, &mut cache);
283
284 let embedded_packages = embed_packages(&resolved_packages, &cache_dir, &mut cache);
285
286 generate_output(
287 &entry_value,
288 &templates_result,
289 &fonts_result,
290 &embedded_packages,
291 &mut cache,
292 compression_level,
293 )
294 .into()
295}
296
297#[proc_macro_derive(IntoValue)]
298pub fn derive_into_value(item: TokenStream) -> TokenStream {
299 let item = parse_macro_input!(item as syn::DeriveInput);
300 derive_intoval::derive_into_value(item)
301 .unwrap_or_else(|err| err.to_compile_error())
302 .into()
303}
304
305#[proc_macro_derive(IntoDict)]
306pub fn derive_into_dict(item: TokenStream) -> TokenStream {
307 let item = parse_macro_input!(item as syn::DeriveInput);
308 derive_intoval::derive_into_dict(item)
309 .unwrap_or_else(|err| err.to_compile_error())
310 .into()
311}