use crate::compression_cache::CompressionCache;
use crate::config::{is_font_file, is_hidden};
use proc_macro2::TokenStream;
use quote::quote;
use std::fs;
use std::path::Path;
#[derive(Default)]
pub struct DirEmbedResult {
pub entries: Vec<TokenStream>,
pub original_size: usize,
pub compressed_size: usize,
pub file_count: usize,
}
impl DirEmbedResult {
pub fn to_dir_code(&self, name: &str) -> TokenStream {
let entries = &self.entries;
quote! {
::typst_bake::__internal::include_dir::Dir::new(#name, &[#(#entries),*])
}
}
}
struct ScanContext<'a, F> {
base: &'a Path,
file_filter: F,
original_size: usize,
compressed_size: usize,
file_count: usize,
cache: &'a mut CompressionCache,
}
impl<'a, F> ScanContext<'a, F>
where
F: Fn(&Path) -> bool + Copy,
{
fn new(base: &'a Path, file_filter: F, cache: &'a mut CompressionCache) -> Self {
Self {
base,
file_filter,
original_size: 0,
compressed_size: 0,
file_count: 0,
cache,
}
}
fn scan_entries(&mut self, current: &Path) -> Vec<TokenStream> {
let mut entries = Vec::new();
let Ok(read_dir) = fs::read_dir(current) else {
return entries;
};
let mut dir_entries: Vec<_> = read_dir.filter_map(Result::ok).collect();
dir_entries.sort_by_key(|e| e.path());
for entry in dir_entries {
let path = entry.path();
if is_hidden(&path) {
continue;
}
let Ok(rel_path) = path.strip_prefix(self.base) else {
continue;
};
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_owned(),
None => rel_path.to_string_lossy().into_owned(),
};
if path.is_file() {
if !(self.file_filter)(&path) {
continue;
}
let file_bytes = fs::read(&path).unwrap_or_else(|e| {
panic!("Failed to read file {}: {e}", path.display());
});
let original_len = file_bytes.len();
let blob_info = self.cache.compress(&file_bytes);
let compressed_len = blob_info.compressed_len;
self.original_size += original_len;
self.compressed_size += compressed_len;
self.file_count += 1;
let blob_ident = quote::format_ident!("BLOB_{}", blob_info.hash);
let abs_path = path
.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.replace('\\', "/");
entries.push(quote! {
::typst_bake::__internal::include_dir::DirEntry::File(
::typst_bake::__internal::include_dir::File::new(
#name,
{
const _: &[u8] = include_bytes!(#abs_path);
&#blob_ident
}
)
)
});
} else if path.is_dir() {
let sub_entries = self.scan_entries(&path);
entries.push(quote! {
::typst_bake::__internal::include_dir::DirEntry::Dir(
::typst_bake::__internal::include_dir::Dir::new(
#name,
&[#(#sub_entries),*]
)
)
});
}
}
entries
}
fn into_result(self, entries: Vec<TokenStream>) -> DirEmbedResult {
DirEmbedResult {
entries,
original_size: self.original_size,
compressed_size: self.compressed_size,
file_count: self.file_count,
}
}
}
fn embed_with_filter(
dir_path: &Path,
filter: impl Fn(&Path) -> bool + Copy,
cache: &mut CompressionCache,
) -> DirEmbedResult {
if !dir_path.exists() {
return DirEmbedResult::default();
}
let mut ctx = ScanContext::new(dir_path, filter, cache);
let entries = ctx.scan_entries(dir_path);
ctx.into_result(entries)
}
pub fn embed_dir(dir_path: &Path, cache: &mut CompressionCache) -> DirEmbedResult {
embed_with_filter(dir_path, |_| true, cache)
}
pub fn embed_fonts_dir(dir_path: &Path, cache: &mut CompressionCache) -> DirEmbedResult {
embed_with_filter(dir_path, is_font_file, cache)
}