Skip to content

Commit e326aef

Browse files
committed
feat: allow multiple assets
1 parent 826d912 commit e326aef

3 files changed

Lines changed: 55 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/builder.rs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@ use crate::navigation::Navigation;
1010
use crate::post_note::PostNote;
1111
use crate::settings::Settings;
1212

13+
/// Builds the static site by rendering templates and copying assets.
14+
///
15+
/// Steps:
16+
/// - Initializes the Tera template engine with HTML templates
17+
/// - Creates the output directory structure
18+
/// - Copies all static asset directories to output
19+
/// - Copies media files referenced in notes
20+
/// - Writes the content map index
21+
/// - Renders all notes using templates
22+
///
23+
/// # Errors
24+
///
25+
/// Returns an error if template loading, directory creation, file copying, or rendering fails.
1326
pub fn build(
14-
notes: &Vec<PostNote>,
27+
notes: &[PostNote],
1528
content_map: ContentMap,
1629
navigation: Navigation,
1730
settings: &Settings,
1831
) -> anyhow::Result<()> {
19-
let tera = Tera::new(&format!("{}/**/*.html", &settings.path.template.display()))?;
20-
21-
fs::create_dir_all(&settings.path.output)?;
22-
copy_static_dir(&settings.path.asset, &settings.path.output)?;
32+
let template_pattern = format!("{}/**/*.html", settings.path.template.display());
33+
let tera = Tera::new(&template_pattern)?;
34+
for asset_path in &settings.path.assets {
35+
copy_static_dir(asset_path, &settings.path.output)?;
36+
}
2337
copy_media_files(notes, &settings.path.input, &settings.path.output)?;
2438
write_content_map(content_map, &settings.path.output)?;
2539
render_notes(notes, &navigation, &tera, &settings.path.output)?;
@@ -28,7 +42,7 @@ pub fn build(
2842
}
2943

3044
fn render_notes(
31-
notes: &Vec<PostNote>,
45+
notes: &[PostNote],
3246
navigation: &Navigation,
3347
tera: &Tera,
3448
output_path: &Path,
@@ -69,37 +83,47 @@ fn render_notes(
6983
Ok(())
7084
}
7185

72-
fn copy_static_dir(src: &Path, destination: &Path) -> io::Result<()> {
73-
fs::create_dir_all(destination)?;
74-
75-
for entry in fs::read_dir(src)? {
86+
/// Recursively copies a directory tree from source to destination.
87+
///
88+
/// Creates the destination directory if it doesn't exist. For each entry in the source:
89+
/// - Directories are recursively copied
90+
/// - Files are copied directly
91+
///
92+
/// If destination already exists, contents are merged (existing files are overwritten).
93+
///
94+
/// # Errors
95+
///
96+
/// Returns an error if any filesystem operation fails (reading, creating directories, copying).
97+
fn copy_static_dir(from: &Path, to: &Path) -> io::Result<()> {
98+
// Ensure the destination directory exists before copying contents.
99+
fs::create_dir_all(to)?;
100+
// Iterate through all entries in the source directory.
101+
for entry in fs::read_dir(from)? {
76102
let entry = entry?;
77-
let file_type = entry.file_type()?;
78-
79-
if file_type.is_dir() {
80-
copy_static_dir(&entry.path(), &destination.join(entry.file_name()))?;
103+
let from = entry.path();
104+
let to = to.join(entry.file_name());
105+
if entry.file_type()?.is_dir() {
106+
// Recursively copy subdirectories.
107+
copy_static_dir(&from, &to)?;
81108
} else {
82-
fs::copy(entry.path(), destination.join(entry.file_name()))?;
109+
fs::copy(&from, &to)?;
83110
}
84111
}
85112

86113
Ok(())
87114
}
88115

89-
fn copy_media_files(notes: &Vec<PostNote>, src: &Path, destination: &Path) -> anyhow::Result<()> {
116+
fn copy_media_files(notes: &[PostNote], src: &Path, destination: &Path) -> anyhow::Result<()> {
90117
fs::create_dir_all(destination)?;
91-
92118
notes.par_iter().for_each(|note| {
93119
note.media_links.par_iter().for_each(|media_link| {
94120
let media_path = PathBuf::from(media_link.to_string());
95121
let output_media_path = PathBuf::from(media_link.to_string());
96-
97122
if let Some(parent) = media_path.parent()
98123
&& let Err(err) = fs::create_dir_all(destination.join(parent))
99124
{
100125
log::warn!("Could not create parent directory: {}", err);
101126
};
102-
103127
if let Err(err) = fs::copy(src.join(&media_path), destination.join(&output_media_path))
104128
{
105129
log::warn!(

src/settings.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ pub struct PathSettings {
2222
pub output: PathBuf,
2323
/// Template directory path.
2424
pub template: PathBuf,
25-
/// Asset directory path.
26-
pub asset: PathBuf,
25+
/// Asset directory paths.
26+
pub assets: Vec<PathBuf>,
2727
}
2828

2929
impl Default for PathSettings {
@@ -32,7 +32,7 @@ impl Default for PathSettings {
3232
input: PathBuf::from(DEFAULT_INPUT_PATH),
3333
output: PathBuf::from(DEFAULT_OUTPUT_PATH),
3434
template: PathBuf::from(DEFAULT_TEMPLATE_PATH),
35-
asset: PathBuf::from(DEFAULT_ASSET_PATH),
35+
assets: vec![PathBuf::from(DEFAULT_ASSET_PATH)],
3636
}
3737
}
3838
}
@@ -58,7 +58,8 @@ struct CliPathSettings {
5858
/// Asset directory path.
5959
#[arg(short, long)]
6060
#[serde(skip_serializing_if = "Option::is_none")]
61-
pub asset: Option<PathBuf>,
61+
#[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')]
62+
pub assets: Option<Vec<PathBuf>>,
6263
}
6364

6465
/// Configurable application settings which get derived from command line
@@ -97,6 +98,7 @@ fn merge_settings(
9798
if let Some(args) = args {
9899
raw_settings = raw_settings.add_source(args);
99100
};
101+
100102
Ok(raw_settings.build()?.try_deserialize::<Settings>()?)
101103
}
102104

@@ -131,6 +133,7 @@ pub fn get_settings() -> Settings {
131133
log::info!(
132134
"Could not load settings from config file or command line arguments, using default settings instead."
133135
);
136+
134137
Settings::default()
135138
}
136139

@@ -146,7 +149,7 @@ mod tests {
146149
path: PathSettings {
147150
input: PathBuf::from("../notes"),
148151
output: DEFAULT_OUTPUT_PATH.into(),
149-
asset: DEFAULT_ASSET_PATH.into(),
152+
assets: vec![DEFAULT_ASSET_PATH.into()],
150153
template: DEFAULT_TEMPLATE_PATH.into(),
151154
},
152155
};
@@ -156,6 +159,7 @@ mod tests {
156159
.build()
157160
.unwrap();
158161
let produced = merge_settings(default_settings, Some(config_file), None).unwrap();
162+
159163
assert_eq!(expected, produced);
160164
}
161165

@@ -165,14 +169,15 @@ mod tests {
165169
path: PathSettings {
166170
input: PathBuf::from("../notes"),
167171
output: DEFAULT_OUTPUT_PATH.into(),
168-
asset: DEFAULT_ASSET_PATH.into(),
172+
assets: vec![DEFAULT_ASSET_PATH.into()],
169173
template: DEFAULT_TEMPLATE_PATH.into(),
170174
},
171175
};
172176
let default_settings = Config::try_from(&Settings::default()).unwrap();
173177
let args = Args::try_parse_from(["post_notes", "-i", "../notes"]).unwrap();
174178
let config_args = Config::try_from(&args).unwrap();
175179
let produced = merge_settings(default_settings, None, Some(config_args)).unwrap();
180+
176181
assert_eq!(expected, produced);
177182
}
178183
}

0 commit comments

Comments
 (0)