use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use log::{debug, error, info, warn};
use nucleusflow::{
FileContentProcessor, HtmlOutputGenerator, HtmlTemplateRenderer,
NucleusFlow, NucleusFlowConfig,
};
use std::{
env,
path::{Path, PathBuf},
process::exit,
};
#[derive(Parser, Debug)]
#[command(
name = "NucleusFlow",
about = "A modern static site generator written in Rust",
version,
author
)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
New {
name: String,
#[arg(short = 't', long, default_value = "blog")]
template: String,
},
Build {
#[arg(short = 'c', long, default_value = "content")]
content_dir: PathBuf,
#[arg(short = 'o', long, default_value = "public")]
output_dir: PathBuf,
#[arg(short = 't', long, default_value = "templates")]
template_dir: PathBuf,
#[arg(short = 'm', long)]
minify: bool,
#[arg(short = 'f', long, default_value = "nucleusflow.toml")]
config: PathBuf,
},
Serve {
#[arg(short = 'p', long, default_value = "3000")]
port: u16,
#[arg(short = 'w', long)]
watch: bool,
#[arg(short = 'd', long, default_value = "public")]
dir: PathBuf,
},
}
fn setup_logging(verbosity: u8) {
let env = env_logger::Env::default();
let mut builder = env_logger::Builder::from_env(env);
let log_level = match verbosity {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
};
builder
.filter_level(log_level)
.format_timestamp(None)
.format_module_path(false)
.init();
debug!("Logging initialized at level: {:?}", log_level);
}
fn handle_new(name: &str, template: &str) -> Result<()> {
info!("Creating new project '{}' with template '{}'", name, template);
if !is_valid_project_name(name) {
error!("Invalid project name: {}", name);
return Err(anyhow::anyhow!(
"Project name must be alphanumeric with hyphens only"
));
}
let project_dir = PathBuf::from(name);
if project_dir.exists() {
error!("Directory already exists: {}", name);
return Err(anyhow::anyhow!("Project directory already exists"));
}
create_project_structure(&project_dir, template).context("Failed to create project structure")?;
info!("Successfully created new project: {}", name);
Ok(())
}
fn is_valid_project_name(name: &str) -> bool {
!name.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
&& !name.starts_with('-')
&& !name.ends_with('-')
}
fn create_project_structure(project_dir: &Path, template: &str) -> Result<()> {
debug!("Creating project structure in: {:?}", project_dir);
let dirs = [
"",
"content",
"templates",
"static",
"themes",
"config",
];
for dir in dirs {
let path = project_dir.join(dir);
std::fs::create_dir_all(&path)
.context(format!("Failed to create directory: {:?}", path))?;
}
let config_content = format!(
r#"[site]
name = "New NucleusFlow Site"
template = "{}"
"#,
template
);
let config_path = project_dir.join("config").join("config.toml");
std::fs::write(&config_path, config_content)
.context("Failed to write config file")?;
if let Err(e) = copy_template_files(project_dir, template) {
warn!("Failed to copy template files: {}", e);
}
Ok(())
}
fn copy_template_files(_project_dir: &Path, template: &str) -> Result<()> {
let template_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("templates")
.join(template);
if !template_dir.exists() {
warn!("Template directory not found: {:?}", template_dir);
return Ok(());
}
Ok(())
}
fn handle_build(
content_dir: PathBuf,
output_dir: PathBuf,
template_dir: PathBuf,
minify: bool,
config_path: PathBuf,
) -> Result<()> {
info!("Building site with configuration:");
info!(" Content directory: {:?}", content_dir);
info!(" Output directory: {:?}", output_dir);
info!(" Template directory: {:?}", template_dir);
info!(" Minification: {}", minify);
info!(" Config file: {:?}", config_path);
let config = NucleusFlowConfig::new(&content_dir, &output_dir, &template_dir)
.context("Failed to create NucleusFlow configuration")?;
let content_processor = FileContentProcessor::new(content_dir);
let template_renderer = HtmlTemplateRenderer::new(template_dir);
let output_generator = HtmlOutputGenerator::new(output_dir);
let nucleus = NucleusFlow::new(
config,
Box::new(content_processor),
Box::new(template_renderer),
Box::new(output_generator),
);
nucleus.process().context("Failed to process site")?;
info!("Site built successfully!");
Ok(())
}
fn handle_serve(port: u16, watch: bool, dir: PathBuf) -> Result<()> {
info!(
"Starting development server on port {} (watch mode: {})",
port, watch
);
info!("Serving directory: {:?}", dir);
if !dir.exists() {
return Err(anyhow::anyhow!(
"Directory does not exist: {:?}",
dir
));
}
info!("Development server functionality not yet implemented");
Ok(())
}
fn main() {
let cli = Cli::parse();
setup_logging(cli.verbose);
let result = match cli.command {
Commands::New { name, template } => handle_new(&name, &template),
Commands::Build {
content_dir,
output_dir,
template_dir,
minify,
config,
} => handle_build(
content_dir,
output_dir,
template_dir,
minify,
config,
),
Commands::Serve { port, watch, dir } => {
handle_serve(port, watch, dir)
}
};
if let Err(err) = result {
error!("Error: {:?}", err);
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Once;
use tempfile::TempDir;
static INIT: Once = Once::new();
fn init_test_logger() {
INIT.call_once(|| {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.init();
});
}
#[test]
fn test_project_name_validation() {
assert!(is_valid_project_name("my-project"));
assert!(is_valid_project_name("project123"));
assert!(!is_valid_project_name(""));
assert!(!is_valid_project_name("-project"));
assert!(!is_valid_project_name("project-"));
assert!(!is_valid_project_name("project!"));
}
#[test]
fn test_project_creation() -> Result<()> {
init_test_logger();
let temp_dir = TempDir::new()?;
let project_path = temp_dir.path().join("test-project");
create_project_structure(&project_path, "blog")?;
let expected_dirs = [
"",
"content",
"templates",
"static",
"themes",
"config",
];
for dir in expected_dirs {
assert!(
project_path.join(dir).exists(),
"Directory {} does not exist",
dir
);
}
let config_content = r#"[site]
name = "New NucleusFlow Site"
template = "blog"
"#;
let config_dir = project_path.join("config");
std::fs::create_dir_all(&config_dir)?;
std::fs::write(config_dir.join("config.toml"), config_content)?;
let config_path = project_path.join("config/config.toml");
assert!(config_path.exists(), "Config file does not exist");
let read_config = std::fs::read_to_string(config_path)?;
assert!(
read_config.contains("template = \"blog\""),
"Config file does not contain expected template setting"
);
Ok(())
}
#[test]
fn test_logging_setup() {
let test_cases = [
(0, log::LevelFilter::Warn),
(1, log::LevelFilter::Info),
(2, log::LevelFilter::Debug),
(3, log::LevelFilter::Trace),
];
for (verbosity, expected_level) in test_cases {
let level = match verbosity {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
};
assert_eq!(level, expected_level, "Incorrect log level for verbosity {}", verbosity);
}
}
}