rs-teststand-autodoc 0.13.0

Generate Markdown documentation from National Instruments TestStand™ sequence files
Documentation
//! Command-line workflow execution runner.

use rs_teststand::Engine;
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli::args::CliArgs;
use crate::cli::format::OutputFormat;
use crate::error::Error;
use crate::extraction::HierarchyExtractor;
use crate::rendering::{Formatter, html_to_pdf, markdown_to_html};

/// Resolves target file path for writing output.
fn resolve_output_path(
    out_path: &Path,
    first_input: Option<&PathBuf>,
    format: OutputFormat,
) -> PathBuf {
    if out_path.is_dir() {
        let base_name = first_input.and_then(|p| p.file_stem()).unwrap_or_default();
        let ext = match format {
            OutputFormat::Markdown => "md",
            OutputFormat::Html => "html",
            OutputFormat::Pdf => "pdf",
        };
        out_path.join(format!("{}.{}", base_name.to_string_lossy(), ext))
    } else {
        out_path.to_path_buf()
    }
}

/// Executes the documentation generation CLI pipeline.
///
/// # Errors
/// Returns [`Error`] or [`std::io::Error`] if reading, generating, or writing fails.
pub fn run_cli(args: &CliArgs) -> Result<(), Box<dyn std::error::Error>> {
    let config = args.to_extractor_config();
    let engine = Engine::new().map_err(Error::from)?;
    let files = HierarchyExtractor::extract(&engine, &args.files, &config)?;

    // The page title is the file's name, never its path. A business document
    // is sent outside the building, and the full path names the machine it was
    // generated on. The browser tab and the printed header both read this.
    let title = files.first().map(|file| {
        let name = Path::new(&file.path)
            .file_name()
            .map_or(file.path.as_str(), |n| n.to_str().unwrap_or(&file.path));
        if config.rules.plain_title {
            name.strip_suffix(".seq").unwrap_or(name).to_owned()
        } else {
            name.to_owned()
        }
    });
    let first_file_name = title.as_deref();

    match args.format {
        OutputFormat::Markdown => {
            let markdown_content = Formatter::generate(&files, &config, Some(&engine));
            write_or_print(args, OutputFormat::Markdown, &markdown_content)?;
        }
        OutputFormat::Html => {
            let markdown_content = Formatter::generate(&files, &config, Some(&engine));
            let html_content = markdown_to_html(&markdown_content, first_file_name);
            write_or_print(args, OutputFormat::Html, &html_content)?;
        }
        OutputFormat::Pdf => {
            let markdown_content = Formatter::generate(&files, &config, Some(&engine));
            let html_content = markdown_to_html(&markdown_content, first_file_name);
            let default_target = PathBuf::from("report.pdf");
            let target_ref = args.output.as_ref().unwrap_or(&default_target);
            let target_path =
                resolve_output_path(target_ref, args.files.first(), OutputFormat::Pdf);
            html_to_pdf(&html_content, &target_path)?;
            println!("Generated documentation: {}", target_path.display());
        }
    }

    Ok(())
}

fn write_or_print(
    args: &CliArgs,
    format: OutputFormat,
    content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    if let Some(ref out_path) = args.output {
        let target_path = resolve_output_path(out_path, args.files.first(), format);
        if let Some(parent) = target_path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&target_path, content)?;
        println!("Generated documentation: {}", target_path.display());
    } else {
        println!("{content}");
    }
    Ok(())
}