quillmark-cli 0.113.0

Command-line interface for Quillmark, a schema-driven document engine
use crate::commands::load_quill;
use crate::errors::{CliError, Result};
use crate::output::{derive_output_path, page_output_path, write_file, write_stdout};
use clap::Parser;
use quillmark::Quillmark;
use quillmark_core::types::{OutputFormat, RenderOptions};
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 card-yaml blocks
    #[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
    #[arg(short, long, value_name = "FORMAT", default_value = "pdf")]
    format: String,

    /// Write output to stdout instead of file
    #[arg(long)]
    stdout: 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<()> {
    let quill = load_quill(&args.quill)?;

    let (parsed, parse_warnings, markdown_path_for_output) =
        if let Some(ref markdown_path) = args.markdown_file {
            if !markdown_path.exists() {
                return Err(CliError::InvalidArgument(format!(
                    "Markdown file not found: {}",
                    markdown_path.display()
                )));
            }

            let markdown = fs::read_to_string(markdown_path)?;
            let output = quill.parse(&markdown)?;

            (output.document, output.warnings, Some(markdown_path.clone()))
        } else {
            (quill.seed_document(), Vec::new(), None)
        };

    let output_format = args
        .format
        .parse::<OutputFormat>()
        .map_err(|e| CliError::InvalidArgument(e.to_string()))?;

    if let Some(data_path) = args.output_data {
        let json_data = quill.compile_data(&parsed).map_err(CliError::Render)?;
        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::other(format!(
                "Failed to write JSON data: {}",
                e
            )))
        })?;
    }

    let engine = Quillmark::new();
    let mut result = engine.render(
        &quill,
        &parsed,
        &RenderOptions::default().with_output_format(output_format),
    )?;

    result.warnings.splice(0..0, parse_warnings);

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

    if result.artifacts.is_empty() {
        return Err(CliError::InvalidArgument(
            "No artifacts produced from rendering".to_string(),
        ));
    }

    if args.stdout {
        if result.artifacts.len() > 1 {
            return Err(CliError::InvalidArgument(format!(
                "{} renders {} pages, one artifact each, and --stdout carries one; \
                 drop --stdout to write the pages as files",
                output_format,
                result.artifacts.len()
            )));
        }
        write_stdout(&result.artifacts[0].bytes)?;
    } else {
        let output_path = args.output.unwrap_or_else(|| {
            if let Some(ref path) = markdown_path_for_output {
                derive_output_path(path, output_format.as_str())
            } else {
                PathBuf::from(format!("example.{}", output_format))
            }
        });
        if let [artifact] = result.artifacts.as_slice() {
            write_file(&output_path, &artifact.bytes, !args.quiet)?;
        } else {
            // Every page numbered, page one included: an unnumbered file beside
            // numbered ones reads as the whole document.
            for (i, artifact) in result.artifacts.iter().enumerate() {
                let path = page_output_path(&output_path, i + 1);
                write_file(&path, &artifact.bytes, !args.quiet)?;
            }
        }
    }

    Ok(())
}