forme 0.1.0

Compile-time HTML template engine — plain HTML templates with tpl-* directives generate type-safe Rust rendering functions
Documentation
use clap::{Args, Parser, Subcommand};
use std::fs;
use std::path::Path;

/// Compile-time HTML template engine
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(flatten_help = true)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    #[command(flatten)]
    build_args: BuildArgs,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Build templates (default when no subcommand given)
    Build(BuildArgs),

    /// Initialize a new forme project with a formefile.ts config
    Init,
}

/// Arguments for the build command
#[derive(Args, Debug, Clone)]
struct BuildArgs {
    /// Source template directory to parse
    #[arg(short, long)]
    source: Option<String>,

    /// Output file path
    #[arg(short, long)]
    output: Option<String>,

    /// Optional transformation script path (JS/TS)
    #[arg(short, long)]
    transform_script: Option<String>,

    /// Optional transformation script path (deprecated, use --transform-script)
    #[arg(long, hide = true)]
    js_script: Option<String>,

    /// Optional escape function
    #[arg(short, long)]
    escape_func: Option<String>,

    /// Path to config file (default: auto-discover formefile.ts or formefile.js)
    #[arg(short, long)]
    config: Option<String>,
}

impl BuildArgs {
    /// Resolve the transform_script field, preferring --transform-script over --js-script
    fn resolve_transform_script(&self) -> Option<&str> {
        self.transform_script
            .as_deref()
            .or(self.js_script.as_deref())
    }
}

fn main() {
    let cli = Cli::parse();

    let result = match cli.command {
        Some(Commands::Build(args)) => run_build(args),
        Some(Commands::Init) => run_init(),
        None => run_build(cli.build_args),
    };

    if let Err(e) = result {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}

fn run_init() -> Result<(), Box<dyn std::error::Error>> {
    let formefile_ts = Path::new("formefile.ts");
    let formefile_js = Path::new("formefile.js");

    if formefile_ts.exists() {
        return Err("formefile.ts already exists".into());
    }
    if formefile_js.exists() {
        return Err("formefile.js already exists".into());
    }

    let content = r#"// formefile.ts — forme template compiler configuration
// Run `forme build` or just `forme` to compile templates.

interface FormeContext {
  cwd: string;
  env: Record<string, string>;
  cli: {
    source?: string;
    output?: string;
    transform_script?: string;
    escape_func?: string;
  };
}

interface FormeConfig {
  source: string;
  output: string;
  transform_script?: string;
  escape_func?: string;
}

export function config(ctx: FormeContext): FormeConfig | FormeConfig[] {
  return {
    // Source template directory
    source: "templates",

    // Output generated Rust file
    output: "src/generated.rs",

    // Optional: JS/TS transform script for custom elements
    // transform_script: "src/components.js",

    // Optional: custom escape function (default: "forme::html_escape")
    // escape_func: "my_crate::escape",
  };
}
"#;

    fs::write(formefile_ts, content)?;
    println!("Created formefile.ts");

    Ok(())
}

fn run_build(args: BuildArgs) -> Result<(), Box<dyn std::error::Error>> {
    let config_file = forme::discover_config_file(args.config.as_deref());

    if let Some(config_path) = config_file {
        println!("Using config: {}", config_path.display());
        let configs = forme::evaluate_config_file(
            &config_path,
            args.source.as_deref(),
            args.output.as_deref(),
            args.resolve_transform_script(),
            args.escape_func.as_deref(),
        )?;

        if configs.is_empty() {
            println!("Config returned no targets, nothing to do.");
            return Ok(());
        }

        for config in &configs {
            forme::Builder::new(&config.source, &config.output)
                .transform_script_option(config.transform_script.as_deref())
                .escape_func_option(config.escape_func.as_deref())
                .build()?;
        }

        Ok(())
    } else {
        let source = args.source.as_deref().ok_or(
            "Missing required argument: --source (or provide a formefile.ts/formefile.js config file)",
        )?;
        let output = args.output.as_deref().ok_or(
            "Missing required argument: --output (or provide a formefile.ts/formefile.js config file)",
        )?;

        let mut builder = forme::Builder::new(source, output);
        if let Some(ts) = args.resolve_transform_script() {
            builder = builder.transform_script(ts);
        }
        if let Some(ef) = args.escape_func.as_deref() {
            builder = builder.escape_func(ef);
        }
        builder.build()?;

        Ok(())
    }
}