xrml 0.1.0

eXtensible Rust Markup Language — recursive acronym: HRML (HRML Markup Language) and TRML (TOML-like Markup Language)
Documentation
use std::env;
use std::path::PathBuf;
use std::process;

mod ast_log;
mod build;
mod project;
mod server;
mod validation;

struct PathCommandOptions {
    path: PathBuf,
    log_ast: bool,
    debug: bool,
}

fn print_help() {
    println!(
        "HRML - Hypertext Rust Markup Language v{}",
        env!("CARGO_PKG_VERSION")
    );
    println!();
    let bin = env!("CARGO_BIN_NAME");
    println!("Usage: {bin} <command> [options]");
    println!();
    println!("Commands:");
    println!("  new <name>          Create a new HRML project");
    println!("  dev [path]          Run development server with auto-reload");
    println!("  serve [path]        Run production server");
    println!("  build [path]        Build static site for deployment");
    println!("  check [path]        Validate templates and configuration");
    println!("  convert [file]      Convert .hrml to .trml syntax");
    println!("  --log-ast           Regenerate ast.log before build/serve/dev");
    println!("  --debug, -d         Enable verbose render diagnostics");
    println!("  version             Show version information");
    println!("  help                Show this help message");
    println!();
    println!("Examples:");
    println!("  {bin} new myapp              Create new project 'myapp'");
    println!("  {bin} dev                    Start dev server in current directory");
    println!("  {bin} serve ./myapp          Serve project from ./myapp");
    println!("  {bin} build ./myapp          Build static site from ./myapp");
    println!("  {bin} check                  Validate current project");
}

fn parse_path_command_options(args: &[String]) -> Result<PathCommandOptions, String> {
    let mut path = None;
    let mut log_ast = false;
    let mut debug = false;

    for arg in args {
        if arg == "--log-ast" {
            log_ast = true;
        } else if arg == "--debug" || arg == "-d" {
            debug = true;
        } else if arg.starts_with('-') {
            return Err(format!("Unknown option: {}", arg));
        } else if path.is_none() {
            path = Some(PathBuf::from(arg));
        } else {
            return Err(format!("Unexpected extra argument: {}", arg));
        }
    }

    Ok(PathCommandOptions {
        path: path.unwrap_or_else(|| PathBuf::from(".")),
        log_ast,
        debug,
    })
}

#[tokio::main]
async fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        print_help();
        return;
    }

    let command = &args[1];

    match command.as_str() {
        "help" | "--help" | "-h" => print_help(),
        "version" | "--version" | "-v" => println!("HRML {}", env!("CARGO_PKG_VERSION")),
        "new" => {
            if args.len() < 3 {
                eprintln!("Error: Project name required");
                eprintln!("Usage: {} new <name>", env!("CARGO_BIN_NAME"));
                process::exit(1);
            }

            let name = &args[2];
            println!("Creating new HRML project: {}", name);
            if let Err(e) = project::create_project(name) {
                eprintln!("Error creating project: {}", e);
                process::exit(1);
            }

            println!("Project '{}' created successfully!", name);
            println!();
            println!("To get started:");
            println!("  cd {}", name);
            println!("  {} dev", env!("CARGO_BIN_NAME"));
        }
        "dev" => {
            let options = match parse_path_command_options(&args[2..]) {
                Ok(options) => options,
                Err(e) => {
                    eprintln!("Error: {}", e);
                    process::exit(1);
                }
            };
            if let Err(e) = server::run_dev(&options.path, options.log_ast, options.debug).await {
                eprintln!("Error: {}", e);
                process::exit(1);
            }
        }
        "serve" => {
            let options = match parse_path_command_options(&args[2..]) {
                Ok(options) => options,
                Err(e) => {
                    eprintln!("Error: {}", e);
                    process::exit(1);
                }
            };
            if let Err(e) = server::serve_static(&options.path, options.log_ast).await {
                eprintln!("Serve error: {}", e);
                process::exit(1);
            }
        }
        "build" => {
            let options = match parse_path_command_options(&args[2..]) {
                Ok(options) => options,
                Err(e) => {
                    eprintln!("Error: {}", e);
                    process::exit(1);
                }
            };
            if let Err(e) = build::build_site(&options.path, options.log_ast) {
                eprintln!("Build failed: {}", e);
                process::exit(1);
            }
        }
        "check" => {
            let options = match parse_path_command_options(&args[2..]) {
                Ok(options) => options,
                Err(e) => {
                    eprintln!("Error: {}", e);
                    process::exit(1);
                }
            };
            if options.log_ast {
                let config = match project::load_project_config(&options.path) {
                    Ok(config) => config,
                    Err(e) => {
                        eprintln!("Check failed: {}", e);
                        process::exit(1);
                    }
                };
                if let Err(e) = ast_log::write_ast_log(&options.path, &config) {
                    eprintln!("Check failed: {}", e);
                    process::exit(1);
                }
            }
            if let Err(e) = project::check_project(&options.path) {
                eprintln!("Check failed: {}", e);
                process::exit(1);
            }
        }
        "convert" => {
            let path = args.get(2).map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."));
            let source = std::fs::read_to_string(&path)
                .unwrap_or_else(|e| { eprintln!("Read error: {e}"); process::exit(1); });
            match xrml::convert::to_trml(&source) {
                Ok(trml) => println!("{trml}"),
                Err(e) => { eprintln!("Convert error: {e}"); process::exit(1); }
            }
        }
        _ => {
            eprintln!("Unknown command: {}", command);
            eprintln!();
            print_help();
            process::exit(1);
        }
    }
}