Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 43 additions & 19 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PostNote>,
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)?;
Expand All @@ -28,7 +42,7 @@ pub fn build(
}

fn render_notes(
notes: &Vec<PostNote>,
notes: &[PostNote],
navigation: &Navigation,
tera: &Tera,
output_path: &Path,
Expand Down Expand Up @@ -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<PostNote>, 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!(
Expand Down
17 changes: 11 additions & 6 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
}

impl Default for PathSettings {
Expand All @@ -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)],
}
}
}
Expand All @@ -58,7 +58,8 @@ struct CliPathSettings {
/// Asset directory path.
#[arg(short, long)]
#[serde(skip_serializing_if = "Option::is_none")]
pub asset: Option<PathBuf>,
#[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')]
pub assets: Option<Vec<PathBuf>>,
}

/// Configurable application settings which get derived from command line
Expand Down Expand Up @@ -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::<Settings>()?)
}

Expand Down Expand Up @@ -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()
}

Expand All @@ -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(),
},
};
Expand All @@ -156,6 +159,7 @@ mod tests {
.build()
.unwrap();
let produced = merge_settings(default_settings, Some(config_file), None).unwrap();

assert_eq!(expected, produced);
}

Expand All @@ -165,14 +169,15 @@ 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(),
},
};
let default_settings = Config::try_from(&Settings::default()).unwrap();
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);
}
}
Loading