use crate::core::error::{ProcessingError, Result};
use clap::{value_parser, Arg, ArgAction, Command};
use log::{debug, info};
use std::fs;
use std::path::PathBuf;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_CONTENT_DIR: &str = "content";
pub const DEFAULT_OUTPUT_DIR: &str = "public";
pub const DEFAULT_TEMPLATE_DIR: &str = "templates";
pub const DEFAULT_PORT: u16 = 3000;
pub fn build() -> Command {
debug!("Building CLI command structure");
Command::new("NucleusFlow")
.author("NucleusFlow Contributors")
.about("A fast and flexible static site generator written in Rust.")
.version(VERSION)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("new")
.about("Create a new project")
.arg(
Arg::new("name")
.help("Name of the new project")
.required(true)
.value_parser(value_parser!(String))
)
.arg(
Arg::new("template")
.short('t')
.long("template")
.help("Template to use (blog, docs, portfolio)")
.value_parser(["blog", "docs", "portfolio"])
.default_value("blog")
)
)
.subcommand(
Command::new("build")
.about("Build the static site")
.arg(
Arg::new("content")
.short('c')
.long("content")
.help("Content directory")
.value_parser(value_parser!(PathBuf))
.default_value(DEFAULT_CONTENT_DIR)
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.help("Output directory")
.value_parser(value_parser!(PathBuf))
.default_value(DEFAULT_OUTPUT_DIR)
)
.arg(
Arg::new("template")
.short('t')
.long("template")
.help("Template directory")
.value_parser(value_parser!(PathBuf))
.default_value(DEFAULT_TEMPLATE_DIR)
)
.arg(
Arg::new("minify")
.short('m')
.long("minify")
.help("Minify output")
.action(ArgAction::SetTrue)
)
)
.after_help(
"\x1b[1;4mDocumentation:\x1b[0m\n\n https://nucleusflow.com\n\n\
\x1b[1;4mLicense:\x1b[0m\n The project is licensed under the terms of \
both the MIT license and the Apache License (Version 2.0)."
)
}
pub fn execute() -> Result<()> {
let matches = build().get_matches();
match matches.subcommand() {
Some(("new", sub_matches)) => {
let name = sub_matches.get_one::<String>("name").unwrap();
let default_template = "blog".to_string();
let template = sub_matches
.get_one::<String>("template")
.unwrap_or(&default_template);
create_new_project(name, template)
}
Some(("build", sub_matches)) => {
let content_dir =
sub_matches.get_one::<PathBuf>("content").unwrap();
let output_dir =
sub_matches.get_one::<PathBuf>("output").unwrap();
let template_dir =
sub_matches.get_one::<PathBuf>("template").unwrap();
let minify = sub_matches.get_flag("minify");
build_site(content_dir, output_dir, template_dir, minify)
}
_ => Err(ProcessingError::internal_error("Unknown command")),
}
}
fn create_new_project(name: &str, template: &str) -> Result<()> {
info!(
"Creating new project '{}' with template '{}'",
name, template
);
if name.is_empty() {
return Err(ProcessingError::configuration(
"Project name cannot be empty",
None,
None,
));
}
Ok(())
}
fn build_site(
content_dir: &PathBuf,
output_dir: &PathBuf,
template_dir: &PathBuf,
minify: bool,
) -> Result<()> {
info!(
"Building site with content at '{:?}', output to '{:?}', and templates in '{:?}'",
content_dir, output_dir, template_dir
);
if !content_dir.exists() {
return Err(ProcessingError::configuration(
"Content directory does not exist",
Some(content_dir.clone()),
None,
));
}
let output_content =
fs::read_to_string(content_dir)?.to_uppercase();
let final_content = if minify {
minify_content(&output_content)
} else {
output_content
};
fs::write(output_dir.join("output.html"), final_content)?;
Ok(())
}
fn minify_content(content: &str) -> String {
content.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn print_banner() {
info!("Displaying NucleusFlow banner");
let title = format!("NucleusFlow 🦀 v{}", VERSION);
let description = "A powerful Rust library for content processing, enabling static site generation, document conversion, and templating.";
let width = title.len().max(description.len()) + 4;
let horizontal_line = "─".repeat(width - 2);
println!("\n┌{}┐", horizontal_line);
println!("│{:^width$}│", title, width = width - 2);
println!("├{}┤", horizontal_line);
println!("│{:^width$}│", description, width = width - 2);
println!("└{}┘\n", horizontal_line);
}
#[cfg(test)]
mod tests {
use super::*;
use clap::ArgMatches;
fn get_matches(args: Vec<&str>) -> ArgMatches {
build().get_matches_from(args)
}
#[test]
fn test_new_command() {
let matches = get_matches(vec![
"nucleusflow",
"new",
"my-site",
"--template",
"blog",
]);
let new_cmd = matches.subcommand_matches("new").unwrap();
assert_eq!(
new_cmd.get_one::<String>("name").unwrap(),
"my-site"
);
assert_eq!(
new_cmd.get_one::<String>("template").unwrap(),
"blog"
);
}
#[test]
fn test_build_command() {
let matches = get_matches(vec![
"nucleusflow",
"build",
"--content",
"content",
"--output",
"public",
"--minify",
]);
let build_cmd = matches.subcommand_matches("build").unwrap();
assert_eq!(
build_cmd.get_one::<PathBuf>("content").unwrap().as_path(),
PathBuf::from("content").as_path()
);
assert!(build_cmd.get_flag("minify"));
}
}