From e326aef32e2d7b3de03547b498e31fb8158e60bf Mon Sep 17 00:00:00 2001 From: Tim-Raphael Date: Tue, 25 Nov 2025 16:03:37 +0100 Subject: [PATCH] feat: allow multiple assets --- Cargo.lock | 2 +- src/builder.rs | 62 ++++++++++++++++++++++++++++++++++--------------- src/settings.rs | 17 +++++++++----- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 418da2b..55a215f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "post-notes" -version = "0.1.1" +version = "0.1.0" dependencies = [ "anyhow", "clap", diff --git a/src/builder.rs b/src/builder.rs index 5379200..34202fa 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -10,16 +10,30 @@ use crate::navigation::Navigation; use crate::post_note::PostNote; use crate::settings::Settings; +/// Builds the static site by rendering templates and copying assets. +/// +/// Steps: +/// - Initializes the Tera template engine with HTML templates +/// - Creates the output directory structure +/// - Copies all static asset directories to output +/// - Copies media files referenced in notes +/// - Writes the content map index +/// - Renders all notes using templates +/// +/// # Errors +/// +/// Returns an error if template loading, directory creation, file copying, or rendering fails. pub fn build( - notes: &Vec, + notes: &[PostNote], content_map: ContentMap, navigation: Navigation, settings: &Settings, ) -> anyhow::Result<()> { - let tera = Tera::new(&format!("{}/**/*.html", &settings.path.template.display()))?; - - fs::create_dir_all(&settings.path.output)?; - copy_static_dir(&settings.path.asset, &settings.path.output)?; + let template_pattern = format!("{}/**/*.html", settings.path.template.display()); + let tera = Tera::new(&template_pattern)?; + for asset_path in &settings.path.assets { + copy_static_dir(asset_path, &settings.path.output)?; + } copy_media_files(notes, &settings.path.input, &settings.path.output)?; write_content_map(content_map, &settings.path.output)?; render_notes(notes, &navigation, &tera, &settings.path.output)?; @@ -28,7 +42,7 @@ pub fn build( } fn render_notes( - notes: &Vec, + notes: &[PostNote], navigation: &Navigation, tera: &Tera, output_path: &Path, @@ -69,37 +83,47 @@ fn render_notes( Ok(()) } -fn copy_static_dir(src: &Path, destination: &Path) -> io::Result<()> { - fs::create_dir_all(destination)?; - - for entry in fs::read_dir(src)? { +/// Recursively copies a directory tree from source to destination. +/// +/// Creates the destination directory if it doesn't exist. For each entry in the source: +/// - Directories are recursively copied +/// - Files are copied directly +/// +/// If destination already exists, contents are merged (existing files are overwritten). +/// +/// # Errors +/// +/// Returns an error if any filesystem operation fails (reading, creating directories, copying). +fn copy_static_dir(from: &Path, to: &Path) -> io::Result<()> { + // Ensure the destination directory exists before copying contents. + fs::create_dir_all(to)?; + // Iterate through all entries in the source directory. + for entry in fs::read_dir(from)? { let entry = entry?; - let file_type = entry.file_type()?; - - if file_type.is_dir() { - copy_static_dir(&entry.path(), &destination.join(entry.file_name()))?; + let from = entry.path(); + let to = to.join(entry.file_name()); + if entry.file_type()?.is_dir() { + // Recursively copy subdirectories. + copy_static_dir(&from, &to)?; } else { - fs::copy(entry.path(), destination.join(entry.file_name()))?; + fs::copy(&from, &to)?; } } Ok(()) } -fn copy_media_files(notes: &Vec, src: &Path, destination: &Path) -> anyhow::Result<()> { +fn copy_media_files(notes: &[PostNote], src: &Path, destination: &Path) -> anyhow::Result<()> { fs::create_dir_all(destination)?; - notes.par_iter().for_each(|note| { note.media_links.par_iter().for_each(|media_link| { let media_path = PathBuf::from(media_link.to_string()); let output_media_path = PathBuf::from(media_link.to_string()); - if let Some(parent) = media_path.parent() && let Err(err) = fs::create_dir_all(destination.join(parent)) { log::warn!("Could not create parent directory: {}", err); }; - if let Err(err) = fs::copy(src.join(&media_path), destination.join(&output_media_path)) { log::warn!( diff --git a/src/settings.rs b/src/settings.rs index 73ea576..19a8ef5 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -22,8 +22,8 @@ pub struct PathSettings { pub output: PathBuf, /// Template directory path. pub template: PathBuf, - /// Asset directory path. - pub asset: PathBuf, + /// Asset directory paths. + pub assets: Vec, } impl Default for PathSettings { @@ -32,7 +32,7 @@ impl Default for PathSettings { input: PathBuf::from(DEFAULT_INPUT_PATH), output: PathBuf::from(DEFAULT_OUTPUT_PATH), template: PathBuf::from(DEFAULT_TEMPLATE_PATH), - asset: PathBuf::from(DEFAULT_ASSET_PATH), + assets: vec![PathBuf::from(DEFAULT_ASSET_PATH)], } } } @@ -58,7 +58,8 @@ struct CliPathSettings { /// Asset directory path. #[arg(short, long)] #[serde(skip_serializing_if = "Option::is_none")] - pub asset: Option, + #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')] + pub assets: Option>, } /// Configurable application settings which get derived from command line @@ -97,6 +98,7 @@ fn merge_settings( if let Some(args) = args { raw_settings = raw_settings.add_source(args); }; + Ok(raw_settings.build()?.try_deserialize::()?) } @@ -131,6 +133,7 @@ pub fn get_settings() -> Settings { log::info!( "Could not load settings from config file or command line arguments, using default settings instead." ); + Settings::default() } @@ -146,7 +149,7 @@ mod tests { path: PathSettings { input: PathBuf::from("../notes"), output: DEFAULT_OUTPUT_PATH.into(), - asset: DEFAULT_ASSET_PATH.into(), + assets: vec![DEFAULT_ASSET_PATH.into()], template: DEFAULT_TEMPLATE_PATH.into(), }, }; @@ -156,6 +159,7 @@ mod tests { .build() .unwrap(); let produced = merge_settings(default_settings, Some(config_file), None).unwrap(); + assert_eq!(expected, produced); } @@ -165,7 +169,7 @@ mod tests { path: PathSettings { input: PathBuf::from("../notes"), output: DEFAULT_OUTPUT_PATH.into(), - asset: DEFAULT_ASSET_PATH.into(), + assets: vec![DEFAULT_ASSET_PATH.into()], template: DEFAULT_TEMPLATE_PATH.into(), }, }; @@ -173,6 +177,7 @@ mod tests { let args = Args::try_parse_from(["post_notes", "-i", "../notes"]).unwrap(); let config_args = Config::try_from(&args).unwrap(); let produced = merge_settings(default_settings, None, Some(config_args)).unwrap(); + assert_eq!(expected, produced); } }