quillmark-cli 0.53.0

Command-line interface for the Quillmark Markdown rendering system
use crate::errors::{CliError, Result};
use crate::output::{derive_output_path, OutputWriter};
use clap::Parser;
use quillmark::{ParsedDocument, Quill, Quillmark};
use quillmark_core::OutputFormat;
use std::fs;
use std::path::PathBuf;

#[derive(Parser)]
pub struct RenderArgs {
    /// Path to quill directory
    #[arg(value_name = "QUILL_PATH")]
    quill: PathBuf,

    /// Path to markdown file with YAML frontmatter
    #[arg(value_name = "MARKDOWN_FILE")]
    markdown_file: Option<PathBuf>,

    /// Output file path (default: derived from input filename)
    #[arg(short, long, value_name = "FILE")]
    output: Option<PathBuf>,

    /// Output format: pdf, svg, png, txt
    #[arg(short, long, value_name = "FORMAT", default_value = "pdf")]
    format: String,

    /// Write output to stdout instead of file
    #[arg(long)]
    stdout: bool,

    /// Show detailed processing information
    #[arg(short, long)]
    verbose: bool,

    /// Suppress all non-error output
    #[arg(long)]
    quiet: bool,

    /// Output intermediate JSON data to file
    #[arg(long, value_name = "DATA_FILE")]
    output_data: Option<PathBuf>,
}

pub fn execute(args: RenderArgs) -> Result<()> {
    // Validate quill path exists
    if !args.quill.exists() {
        return Err(CliError::InvalidArgument(format!(
            "Quill directory not found: {}",
            args.quill.display()
        )));
    }

    if args.verbose {
        println!("Loading quill from: {}", args.quill.display());
    }

    // Load quill
    let quill = Quill::from_path(args.quill.clone())?;

    if args.verbose {
        println!("Quill loaded: {}", quill.name);
    }

    // Determine if we have a markdown file or need to use example content
    let (parsed, markdown_path_for_output) = if let Some(ref markdown_path) = args.markdown_file {
        // Validate markdown file exists
        if !markdown_path.exists() {
            return Err(CliError::InvalidArgument(format!(
                "Markdown file not found: {}",
                markdown_path.display()
            )));
        }

        if args.verbose {
            println!("Reading markdown from: {}", markdown_path.display());
        }

        // Read markdown file
        let markdown = fs::read_to_string(markdown_path)?;

        // Parse markdown
        let parsed = ParsedDocument::from_markdown(&markdown)?;

        if args.verbose {
            println!("Markdown parsed successfully");
        }
        (parsed, Some(markdown_path.clone()))
    } else {
        // Get example content
        let markdown = quill.example.clone().ok_or_else(|| {
            CliError::InvalidArgument(format!(
                "Quill '{}' does not have example content",
                quill.name
            ))
        })?;

        if args.verbose {
            println!("Using example content from quill");
        }

        // Parse markdown
        let parsed = ParsedDocument::from_markdown(&markdown)?;

        if args.verbose {
            println!("Example markdown parsed successfully");
        }

        (parsed, None)
    };

    // Create engine and workflow
    let engine = Quillmark::new();
    let workflow = engine.workflow(&quill)?;

    if args.verbose {
        println!("Workflow created for backend: {}", workflow.backend_id());
    }

    // Parse output format
    let output_format = match args.format.to_lowercase().as_str() {
        "pdf" => OutputFormat::Pdf,
        "svg" => OutputFormat::Svg,
        "png" => OutputFormat::Png,
        "txt" => OutputFormat::Txt,
        _ => {
            return Err(CliError::InvalidArgument(format!(
                "Invalid output format: {}. Must be one of: pdf, svg, png, txt",
                args.format
            )));
        }
    };

    if args.verbose {
        println!("Rendering to format: {:?}", output_format);
    }

    // Handle output-data
    if let Some(data_path) = args.output_data {
        let json_data = workflow
            .compile_data(&parsed)
            .map_err(|e| CliError::Render(e))?;
        let f = std::fs::File::create(&data_path).map_err(|e| {
            CliError::Io(std::io::Error::new(
                e.kind(),
                format!(
                    "Failed to create data output file '{}': {}",
                    data_path.display(),
                    e
                ),
            ))
        })?;
        serde_json::to_writer_pretty(f, &json_data).map_err(|e| {
            CliError::Io(std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("Failed to write JSON data: {}", e),
            ))
        })?;
        if args.verbose && !args.quiet {
            println!("JSON data written to: {}", data_path.display());
        }
    }

    // Render
    let result = workflow.render(&parsed, Some(output_format))?;

    // Display warnings if any
    if !result.warnings.is_empty() && !args.quiet {
        crate::errors::print_warnings(&result.warnings);
    }

    // Get the first artifact (there should only be one for single format render)
    let artifact = result.artifacts.first().ok_or_else(|| {
        CliError::InvalidArgument("No artifacts produced from rendering".to_string())
    })?;

    // Determine output path
    let output_path = if args.stdout {
        None
    } else {
        Some(args.output.unwrap_or_else(|| {
            if let Some(ref path) = markdown_path_for_output {
                derive_output_path(path, &args.format)
            } else {
                PathBuf::from(format!("example.{}", args.format))
            }
        }))
    };

    let writer = OutputWriter::new(args.stdout, output_path, args.quiet);
    writer.write(&artifact.bytes)?;

    if args.verbose && !args.quiet {
        println!("Rendering completed successfully");
    }

    Ok(())
}