libreoffice-pure 0.2.0

Pure-Rust LibreOffice-compatible document generation CLI
use std::env;
use std::fs;
use std::path::Path;

use lo_base::{database_from_csv, execute_select};
use lo_calc::{evaluate_formula, save_ods, workbook_from_csv_opts};
use lo_core::{CellValue, Result, Sheet, TextDocument};
use lo_draw::demo_drawing;
use lo_impress::demo_presentation;
use lo_math::{from_latex, to_mathml_string};
use lo_uno::{EchoService, ServiceRegistry, UnoValue};
use lo_writer::{from_markdown, save_odt};
use lo_zip::list_entries;

fn main() {
    if let Err(error) = run() {
        eprintln!("error: {error}");
        std::process::exit(1);
    }
}

fn run() -> Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        print_usage();
        return Ok(());
    }
    match args[1].as_str() {
        "writer" => writer_command(&args[2..]),
        "calc" => calc_command(&args[2..]),
        "impress" => impress_command(&args[2..]),
        "draw" => draw_command(&args[2..]),
        "math" => math_command(&args[2..]),
        "base" => base_command(&args[2..]),
        "package" => package_command(&args[2..]),
        "uno" => uno_command(&args[2..]),
        "help" | "--help" | "-h" => {
            print_usage();
            Ok(())
        }
        other => Err(lo_core::LoError::InvalidInput(format!(
            "unknown command: {other}"
        ))),
    }
}

fn writer_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("new") => {
            let output = required_positional(args, 1, "output path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| "Document".to_string());
            let text = flag_value(args, "--text").unwrap_or_else(|| "Hello from libreoffice-rs".to_string());
            let mut doc = TextDocument::new(title);
            doc.push_paragraph(text);
            save_odt(output, &doc)
        }
        Some("markdown-to-odt") => {
            let input = required_positional(args, 1, "input markdown path")?;
            let output = required_positional(args, 2, "output odt path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| file_stem_or_default(input, "Document"));
            let markdown = fs::read_to_string(input)?;
            let doc = from_markdown(title, &markdown);
            save_odt(output, &doc)
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "writer commands: new, markdown-to-odt".to_string(),
        )),
    }
}

fn calc_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("csv-to-ods") => {
            let input = required_positional(args, 1, "input csv path")?;
            let output = required_positional(args, 2, "output ods path")?;
            let sheet_name = flag_value(args, "--sheet").unwrap_or_else(|| "Sheet1".to_string());
            let title = flag_value(args, "--title").unwrap_or_else(|| file_stem_or_default(output, "Workbook"));
            let has_header = has_flag(args, "--has-header");
            let csv = fs::read_to_string(input)?;
            let workbook = workbook_from_csv_opts(title, &sheet_name, &csv, has_header)?;
            save_ods(output, &workbook)
        }
        Some("eval") => {
            let formula = required_positional(args, 1, "formula")?;
            let has_header = has_flag(args, "--has-header");
            let sheet = if let Some(csv_path) = flag_value(args, "--csv") {
                let csv = fs::read_to_string(csv_path)?;
                let workbook = workbook_from_csv_opts("Eval", "Sheet1", &csv, has_header)?;
                workbook.sheets[0].clone()
            } else {
                let mut sheet = Sheet::new("Sheet1");
                sheet.set(lo_core::CellAddr::new(0, 0), CellValue::Number(1.0));
                sheet.set(lo_core::CellAddr::new(1, 0), CellValue::Number(2.0));
                sheet.set(lo_core::CellAddr::new(2, 0), CellValue::Number(3.0));
                sheet
            };
            let value = evaluate_formula(formula, &sheet)?;
            println!("{value:?}");
            Ok(())
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "calc commands: csv-to-ods, eval".to_string(),
        )),
    }
}

fn impress_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("demo") => {
            let output = required_positional(args, 1, "output odp path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| "Demo Deck".to_string());
            let presentation = demo_presentation(&title);
            lo_impress::save_odp(output, &presentation)
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "impress commands: demo".to_string(),
        )),
    }
}

fn draw_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("demo") => {
            let output = required_positional(args, 1, "output odg path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| "Diagram".to_string());
            let drawing = demo_drawing(&title);
            lo_draw::save_odg(output, &drawing)
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "draw commands: demo".to_string(),
        )),
    }
}

fn math_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("latex-to-mathml") => {
            let input = required_positional(args, 1, "input formula path")?;
            let latex = fs::read_to_string(input)?;
            let doc = from_latex("Formula", &latex)?;
            println!("{}", to_mathml_string(&doc.root));
            Ok(())
        }
        Some("latex-to-odf") => {
            let input = required_positional(args, 1, "input formula path")?;
            let output = required_positional(args, 2, "output odf path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| "Formula".to_string());
            let latex = fs::read_to_string(input)?;
            let doc = from_latex(title, &latex)?;
            lo_odf::save_formula_document(output, &doc)
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "math commands: latex-to-mathml, latex-to-odf".to_string(),
        )),
    }
}

fn base_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("csv-to-odb") => {
            let input = required_positional(args, 1, "input csv path")?;
            let table = required_positional(args, 2, "table name")?;
            let output = required_positional(args, 3, "output odb path")?;
            let title = flag_value(args, "--title").unwrap_or_else(|| file_stem_or_default(output, "Database"));
            let csv = fs::read_to_string(input)?;
            let database = database_from_csv(title, table, &csv)?;
            lo_base::save_odb(output, &database)
        }
        Some("query") => {
            let input = required_positional(args, 1, "input csv path")?;
            let table = required_positional(args, 2, "table name")?;
            let sql = args.iter().skip(3).cloned().collect::<Vec<_>>().join(" ");
            if sql.trim().is_empty() {
                return Err(lo_core::LoError::InvalidInput("missing SQL query".to_string()));
            }
            let csv = fs::read_to_string(input)?;
            let database = database_from_csv("Query", table, &csv)?;
            let result = execute_select(&database, &sql)?;
            println!("{}", result.columns.join("\t"));
            for row in result.rows {
                let cells = row
                    .into_iter()
                    .map(|value| match value {
                        lo_core::DbValue::Null => String::new(),
                        lo_core::DbValue::Integer(v) => v.to_string(),
                        lo_core::DbValue::Float(v) => v.to_string(),
                        lo_core::DbValue::Text(v) => v,
                        lo_core::DbValue::Bool(v) => v.to_string(),
                    })
                    .collect::<Vec<_>>();
                println!("{}", cells.join("\t"));
            }
            Ok(())
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "base commands: csv-to-odb, query".to_string(),
        )),
    }
}

fn package_command(args: &[String]) -> Result<()> {
    match args.first().map(String::as_str) {
        Some("inspect") => {
            let path = required_positional(args, 1, "package path")?;
            for entry in list_entries(path)? {
                println!(
                    "{}\t{} bytes",
                    entry.name, entry.uncompressed_size
                );
            }
            Ok(())
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "package commands: inspect".to_string(),
        )),
    }
}

fn uno_command(args: &[String]) -> Result<()> {
    let mut registry = ServiceRegistry::default();
    registry.register(Box::new(EchoService));
    match args.first().map(String::as_str) {
        Some("list") => {
            for service in registry.list_services() {
                println!("{service}");
            }
            Ok(())
        }
        Some("demo") => {
            let response = registry.invoke(
                "com.libreoffice_rs.Echo",
                "echo",
                &[UnoValue::string("hello from libreoffice-rs")],
            )?;
            println!("{response:?}");
            Ok(())
        }
        _ => Err(lo_core::LoError::InvalidInput(
            "uno commands: list, demo".to_string(),
        )),
    }
}

fn required_positional<'a>(args: &'a [String], index: usize, name: &str) -> Result<&'a str> {
    args.get(index)
        .map(String::as_str)
        .ok_or_else(|| lo_core::LoError::InvalidInput(format!("missing {name}")))
}

fn flag_value(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|arg| arg == flag)
        .and_then(|index| args.get(index + 1).cloned())
}

fn has_flag(args: &[String], flag: &str) -> bool {
    args.iter().any(|arg| arg == flag)
}

fn file_stem_or_default(path: &str, fallback: &str) -> String {
    Path::new(path)
        .file_stem()
        .and_then(|value| value.to_str())
        .map(|value| value.to_string())
        .unwrap_or_else(|| fallback.to_string())
}

fn print_usage() {
    println!(
        "Usage:\n  libreoffice-rs writer new <out.odt> [--title TITLE] [--text TEXT]\n  libreoffice-rs writer markdown-to-odt <in.md> <out.odt> [--title TITLE]\n  libreoffice-rs calc csv-to-ods <in.csv> <out.ods> [--sheet NAME] [--title TITLE] [--has-header]\n  libreoffice-rs calc eval <formula> [--csv input.csv] [--has-header]\n  libreoffice-rs impress demo <out.odp> [--title TITLE]\n  libreoffice-rs draw demo <out.odg> [--title TITLE]\n  libreoffice-rs math latex-to-mathml <formula.txt>\n  libreoffice-rs math latex-to-odf <formula.txt> <out.odf> [--title TITLE]\n  libreoffice-rs base csv-to-odb <in.csv> <table> <out.odb> [--title TITLE]\n  libreoffice-rs base query <in.csv> <table> <SQL...>\n  libreoffice-rs package inspect <file>\n  libreoffice-rs uno list\n  libreoffice-rs uno demo"
    );
}