use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::task::JoinSet;
use crate::default_output_dest;
use crate::templates::{render_css_with_hbs_context, render_html_with_hbs_context};
use crate::types::GenerateWebfontsResult;
enum OutputContents {
Bytes(Arc<Vec<u8>>),
Text(Arc<String>),
Owned(Vec<u8>),
}
impl OutputContents {
fn as_bytes(&self) -> &[u8] {
match self {
Self::Bytes(bytes) => bytes.as_slice(),
Self::Text(text) => text.as_bytes(),
Self::Owned(bytes) => bytes.as_slice(),
}
}
}
impl AsRef<[u8]> for OutputContents {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
struct OutputFile {
path: String,
contents: OutputContents,
skip_unchanged: bool,
}
fn collect_write_outputs(result: &GenerateWebfontsResult) -> std::io::Result<Vec<OutputFile>> {
let mut outputs = Vec::new();
let font_name = result.options.font_name.clone();
let dest = result.options.dest.clone();
if let Some(svg_font) = &result.fonts.svg_font {
outputs.push(OutputFile {
path: default_output_dest(&dest, &font_name, "svg"),
contents: OutputContents::Text(Arc::clone(svg_font)),
skip_unchanged: false,
});
}
if let Some(ttf_font) = &result.fonts.ttf_font {
outputs.push(OutputFile {
path: default_output_dest(&dest, &font_name, "ttf"),
contents: OutputContents::Bytes(Arc::clone(ttf_font)),
skip_unchanged: false,
});
}
if let Some(woff_font) = &result.fonts.woff_font {
outputs.push(OutputFile {
path: default_output_dest(&dest, &font_name, "woff"),
contents: OutputContents::Bytes(Arc::clone(woff_font)),
skip_unchanged: false,
});
}
if let Some(woff2_font) = &result.fonts.woff2_font {
outputs.push(OutputFile {
path: default_output_dest(&dest, &font_name, "woff2"),
contents: OutputContents::Bytes(Arc::clone(woff2_font)),
skip_unchanged: false,
});
}
if let Some(eot_font) = &result.fonts.eot_font {
outputs.push(OutputFile {
path: default_output_dest(&dest, &font_name, "eot"),
contents: OutputContents::Bytes(Arc::clone(eot_font)),
skip_unchanged: false,
});
}
if result.options.css || result.options.html {
let cached = result.get_cached_io()?;
if result.options.css {
let ctx = cached.css_hbs_context.lock().unwrap();
let css = render_css_with_hbs_context(&cached.shared, &ctx, &cached.css_context)?;
drop(ctx);
outputs.push(OutputFile {
path: result.options.css_dest.clone(),
contents: OutputContents::Owned(css.into_bytes()),
skip_unchanged: true,
});
}
if result.options.html {
let ctx = cached.html_hbs_context.lock().unwrap();
let html = render_html_with_hbs_context(
cached.html_registry.as_ref(),
&ctx,
&cached.html_context,
)?;
drop(ctx);
outputs.push(OutputFile {
path: result.options.html_dest.clone(),
contents: OutputContents::Owned(html.into_bytes()),
skip_unchanged: true,
});
}
}
Ok(outputs)
}
pub(crate) async fn write_generate_webfonts_result(
result: &GenerateWebfontsResult,
) -> std::io::Result<Option<HashMap<String, [u8; 16]>>> {
let mut tasks = JoinSet::new();
let mut written = result.options.incremental.then(HashMap::new);
for output in collect_write_outputs(result)? {
if output.skip_unchanged {
record_written_output(&mut written, &output.path, output.contents.as_bytes());
}
tasks.spawn(async move { write_output_file(output.path, output.contents).await });
}
while let Some(result) = tasks.join_next().await {
result.map_err(|error| {
std::io::Error::other(format!("Native write task failed: {error}"))
})??;
}
Ok(written)
}
fn record_written_output(
written: &mut Option<HashMap<String, [u8; 16]>>,
path: &str,
bytes: &[u8],
) {
if let Some(written) = written {
written.insert(path.to_owned(), output_hash(bytes));
}
}
async fn write_output_file(path: String, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
if let Some(parent) = Path::new(&path).parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(path, contents).await
}
fn output_hash(bytes: &[u8]) -> [u8; 16] {
md5::compute(bytes).0
}
pub(crate) fn write_generate_webfonts_result_sync(
result: &mut GenerateWebfontsResult,
) -> std::io::Result<()> {
let outputs = collect_write_outputs(result)?;
let written = &mut result.written_outputs;
for output in outputs {
if output.skip_unchanged {
write_output_file_if_changed(written, output.path, output.contents.as_bytes())?;
} else {
write_output_file_sync(output.path, output.contents.as_bytes())?;
}
}
Ok(())
}
fn write_output_file_sync(path: String, bytes: &[u8]) -> std::io::Result<()> {
if let Some(parent) = Path::new(&path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, bytes)
}
fn write_output_file_if_changed(
written: &mut HashMap<String, [u8; 16]>,
path: String,
bytes: &[u8],
) -> std::io::Result<()> {
let hash = output_hash(bytes);
if written.get(&path) == Some(&hash) {
return Ok(());
}
if let Some(parent) = Path::new(&path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, bytes)?;
written.insert(path, hash);
Ok(())
}