krik 0.1.27

A fast static site generator written in Rust with internationalization, theming, and modern web features
Documentation
use super::validate::{
    ensure_directory, normalize_path, parse_port, validate_directory, validate_theme_dir,
};
use crate::content::{create_page, create_post};
use crate::error::{KrikError, KrikResult, ServerError, ServerErrorKind};
use crate::generator::SiteGenerator;
use crate::init::init_site;
use crate::lint::{generate_html_report, lint_content, lint_content_with_links};
use crate::logging;
use crate::server::DevServer;
use crate::site::SiteConfig;
use clap::ArgMatches;
use std::path::{Path, PathBuf};
use tracing::{debug, error, info, warn};

/// Resolve theme path with priority: command line > site.toml > default
fn resolve_theme_path(cli_theme: Option<&str>, source_dir: &Path) -> KrikResult<Option<PathBuf>> {
    // Priority 1: Command line theme (if provided)
    if let Some(theme_path) = cli_theme {
        info!("Using command line theme: {}", theme_path);
        return validate_theme_dir(
            Some(theme_path),
            "Validating --theme directory from command line",
        );
    }

    // Priority 2: site.toml theme (if provided)
    if let Ok(site_config) = SiteConfig::load_from_path(source_dir) {
        if let Some(theme_path) = &site_config.theme {
            info!("Using theme from site.toml: {}", theme_path);
            return validate_theme_dir(
                Some(theme_path.as_str()),
                "Validating theme directory from site.toml",
            );
        }
    }

    // Priority 3: Default theme
    info!("Using default theme: themes/default");
    Ok(Some(PathBuf::from("themes/default")))
}

/// Handle the server subcommand
pub async fn handle_server(server_matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("server");
    let _enter = _span.enter();

    let input_dir = validate_directory(
        server_matches
            .get_one::<String>("input")
            .map(|s| s.as_str())
            .unwrap_or("content"),
        "Validating --input directory for server",
    )?;
    let output_dir = ensure_directory(
        server_matches
            .get_one::<String>("output")
            .map(|s| s.as_str())
            .unwrap_or("_site"),
        "Ensuring --output directory for server",
    )?;

    // Resolve theme with priority: command line > site.toml > default
    let theme_dir = resolve_theme_path(
        server_matches
            .get_one::<String>("theme")
            .map(|s| s.as_str()),
        &input_dir,
    )?;

    let port = parse_port(
        server_matches
            .get_one::<String>("port")
            .map(|s| s.as_str())
            .unwrap_or("3000"),
        "Parsing --port value for server",
    )?;
    let no_live_reload = server_matches.get_flag("no-live-reload");

    info!("Starting development server on port {}", port);
    debug!("Input directory: {}", input_dir.display());
    debug!("Output directory: {}", output_dir.display());
    debug!(
        "Theme directory: {:?}",
        theme_dir.as_ref().map(|p| p.display())
    );
    debug!("Live reload: {}", !no_live_reload);

    let server = DevServer::new(input_dir, output_dir, theme_dir, port, !no_live_reload);
    server
        .start()
        .await
        .map_err(|e| match e.downcast::<std::io::Error>() {
            Ok(io_err) => KrikError::Server(Box::new(ServerError {
                kind: ServerErrorKind::BindError {
                    port,
                    source: *io_err,
                },
                context: format!("Starting development server on port {port}"),
            })),
            Err(other_err) => KrikError::Server(Box::new(ServerError {
                kind: ServerErrorKind::WebSocketError(other_err.to_string()),
                context: "Starting development server".to_string(),
            })),
        })?;
    Ok(())
}

/// Handle the init subcommand
pub fn handle_init(init_matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("init");
    let _enter = _span.enter();

    let directory = normalize_path(
        init_matches
            .get_one::<String>("directory")
            .map(|s| s.as_str())
            .unwrap_or("."),
        false,
        "Normalizing target directory for init",
    )?;
    let force = init_matches.get_flag("force");

    info!("Initializing new Krik site in: {}", directory.display());
    debug!("Force overwrite: {}", force);

    init_site(&directory, force)
}

/// Handle the post subcommand
pub fn handle_post(post_matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("post");
    let _enter = _span.enter();

    let title = post_matches
        .get_one::<String>("title")
        .map(|s| s.as_str())
        .unwrap_or("New post");
    let filename = post_matches.get_one::<String>("filename");
    let content_dir = ensure_directory(
        post_matches
            .get_one::<String>("content-dir")
            .map(|s| s.as_str())
            .unwrap_or("content"),
        "Ensuring content directory for post",
    )?;

    info!("Creating new post: {}", title);
    debug!("Content directory: {}", content_dir.display());
    debug!("Custom filename: {:?}", filename);

    create_post(&content_dir, title, filename)
}

/// Handle the page subcommand
pub fn handle_page(page_matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("page");
    let _enter = _span.enter();

    let title = page_matches
        .get_one::<String>("title")
        .map(|s| s.as_str())
        .unwrap_or("New page");
    let filename = page_matches.get_one::<String>("filename");
    let content_dir = ensure_directory(
        page_matches
            .get_one::<String>("content-dir")
            .map(|s| s.as_str())
            .unwrap_or("content"),
        "Ensuring content directory for page",
    )?;

    info!("Creating new page: {}", title);
    debug!("Content directory: {}", content_dir.display());
    debug!("Custom filename: {:?}", filename);

    create_page(&content_dir, title, filename)
}

/// Handle the lint subcommand
pub async fn handle_lint(lint_matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("lint");
    let _enter = _span.enter();

    let input_dir = validate_directory(
        lint_matches
            .get_one::<String>("input")
            .map(|s| s.as_str())
            .unwrap_or("content"),
        "Validating --input directory for lint",
    )?;
    let strict = lint_matches.get_flag("strict");
    let check_links = lint_matches.get_flag("check-links");
    let create_report = lint_matches.get_flag("create-report");
    let _verbose = lint_matches.get_flag("verbose");

    info!("🔎 Linting content in: {}", input_dir.display());
    debug!("Strict mode: {}", strict);
    debug!("Check links: {}", check_links);
    debug!("Create report: {}", create_report);
    debug!("Starting content validation...");
    debug!("Verbose logging enabled");

    let report = if check_links {
        info!("🔗 Checking links for validity...");
        lint_content_with_links(&input_dir).await?
    } else {
        lint_content(&input_dir)?
    };

    info!("Scanned {} file(s)", report.files_scanned);
    debug!("Validation completed successfully");

    // Generate HTML report if requested
    if create_report {
        match generate_html_report(&report, check_links) {
            Ok(filename) => {
                info!("📄 HTML report generated: {}", filename);
            }
            Err(e) => {
                warn!("Failed to generate HTML report: {}", e);
            }
        }
    }

    if !report.warnings.is_empty() {
        warn!("Found {} warning(s):", report.warnings.len());
        for w in &report.warnings {
            warn!("  - {}", w);
        }
    }

    // Display broken links if any were found
    if !report.broken_links.is_empty() {
        error!("Found {} broken link(s):", report.broken_links.len());
        for broken_link in &report.broken_links {
            error!(
                "  - {}:{} - {} ({})",
                broken_link.file_path.display(),
                broken_link.line_number,
                broken_link.url,
                broken_link.error
            );
        }
    }

    let has_failures = !report.errors.is_empty()
        || !report.broken_links.is_empty()
        || (strict && !report.warnings.is_empty());

    if has_failures {
        if !report.errors.is_empty() {
            error!("Found {} error(s):", report.errors.len());
            for e in &report.errors {
                error!("  - {}", e);
            }
        }
        if strict && !report.warnings.is_empty() {
            error!(
                "Strict mode: treating {} warning(s) as error(s)",
                report.warnings.len()
            );
        }
        // Return a content validation error
        return Err(KrikError::Content(Box::new(crate::error::ContentError {
            kind: crate::error::ContentErrorKind::ValidationFailed({
                let mut msgs = report.errors.clone();
                if strict {
                    msgs.extend(report.warnings.clone());
                }
                // Add broken links to error messages
                for broken_link in &report.broken_links {
                    msgs.push(format!(
                        "{}:{} - Broken link: {} ({})",
                        broken_link.file_path.display(),
                        broken_link.line_number,
                        broken_link.url,
                        broken_link.error
                    ));
                }
                msgs
            }),
            path: None,
            context: "Content lint failed".to_string(),
        })));
    }

    if check_links {
        info!("✅ No lint errors or broken links found");
    } else {
        info!("✅ No lint errors found");
    }
    Ok(())
}

/// Handle the default generate command
pub fn handle_generate(matches: &ArgMatches) -> KrikResult<()> {
    let _span = logging::get_logger("generate");
    let _enter = _span.enter();

    let input_dir = validate_directory(
        matches
            .get_one::<String>("input")
            .map(|s| s.as_str())
            .unwrap_or("content"),
        "Validating --input directory for generate",
    )?;
    let output_dir = ensure_directory(
        matches
            .get_one::<String>("output")
            .map(|s| s.as_str())
            .unwrap_or("_site"),
        "Ensuring --output directory for generate",
    )?;

    // Resolve theme with priority: command line > site.toml > default
    let theme_dir = resolve_theme_path(
        matches.get_one::<String>("theme").map(|s| s.as_str()),
        &input_dir,
    )?;

    info!("Scanning files in: {}", input_dir.display());
    info!("Output directory: {}", output_dir.display());
    debug!(
        "Theme directory: {:?}",
        theme_dir.as_ref().map(|p| p.display())
    );

    let generator = SiteGenerator::new(&input_dir, &output_dir, theme_dir.as_ref())
        .map_err(|e| match &e {
            KrikError::Theme(theme_err) => {
                error!("Theme Error: {theme_err}");
                error!("Suggestion: Check that the theme directory exists and contains required templates");
                e
            }
            _ => e,
        })?;

    generator.generate_site().map_err(|e| {
        match &e {
            KrikError::Generation(gen_err) => match &gen_err.kind {
                crate::error::GenerationErrorKind::NoContent => {
                    error!("No Content Error: {e}");
                    error!("Suggestion: Check that the content directory contains markdown files");
                }
                _ => {
                    error!("Generation Error: {e}");
                }
            },
            KrikError::Io(_) => {
                error!("IO Error: {e}");
                error!("Suggestion: Check file permissions and disk space");
            }
            KrikError::Markdown(_) => {
                error!("Markdown Error: {e}");
                error!("Suggestion: Fix the markdown or front matter syntax error");
            }
            _ => {
                error!("Error: {e}");
            }
        }
        e
    })?;
    info!("Site generated successfully!");

    Ok(())
}