rwml 0.1.1

Native Rust toolkit for Microsoft Word — read, write, edit, and render legacy .doc (Word 97-2003, [MS-DOC]) and modern .docx (OOXML): one document model, package-preserving edits, field evaluation, Markdown/HTML export, PDF preview
Documentation
use std::process::ExitCode;

fn main() -> ExitCode {
    let mut args = std::env::args().skip(1);
    let Some(command) = args.next() else {
        eprintln!("{}", usage());
        return ExitCode::from(64);
    };

    match command.as_str() {
        "extract" => {
            let Some(path) = args.next() else {
                eprintln!("usage: rwml extract <file.doc|.docx>");
                return ExitCode::from(64);
            };
            match read_file(&path).and_then(|bytes| rwml::extract_text(&bytes)) {
                Ok(text) => {
                    print!("{text}");
                    ExitCode::SUCCESS
                }
                Err(e) => {
                    eprintln!("{path}: {e}");
                    ExitCode::from(1)
                }
            }
        }
        "diagnose" => {
            let Some(path) = args.next() else {
                eprintln!("usage: rwml diagnose <file.doc|.docx>");
                return ExitCode::from(64);
            };
            match read_file(&path).and_then(|bytes| rwml::Document::open(&bytes)) {
                Ok(doc) => {
                    println!("{}", doc.report().to_json());
                    ExitCode::SUCCESS
                }
                Err(e) => {
                    eprintln!("{path}: {e}");
                    ExitCode::from(1)
                }
            }
        }
        "convert" => {
            let Some(path) = args.next() else {
                eprintln!("usage: rwml convert <file.doc|.docx> [txt|md|html]");
                return ExitCode::from(64);
            };
            let format = args.next().unwrap_or_else(|| "md".to_string());
            match read_file(&path).and_then(|bytes| rwml::Document::open(&bytes)) {
                Ok(doc) => match format.as_str() {
                    "txt" => {
                        println!("{}", doc.text());
                        ExitCode::SUCCESS
                    }
                    "md" => {
                        println!("{}", doc.to_markdown());
                        ExitCode::SUCCESS
                    }
                    "html" => {
                        println!("{}", doc.to_html());
                        ExitCode::SUCCESS
                    }
                    _ => {
                        eprintln!("unknown convert format {format:?}; expected txt, md, or html");
                        ExitCode::from(64)
                    }
                },
                Err(e) => {
                    eprintln!("{path}: {e}");
                    ExitCode::from(1)
                }
            }
        }
        "to-docx" => {
            let Some(input) = args.next() else {
                eprintln!("usage: rwml to-docx <input.doc|.docx> <output.docx>");
                return ExitCode::from(64);
            };
            let Some(output) = args.next() else {
                eprintln!("usage: rwml to-docx <input.doc|.docx> <output.docx>");
                return ExitCode::from(64);
            };
            match read_file(&input)
                .and_then(|bytes| rwml::Document::open(&bytes))
                .map(|doc| rwml::write_docx(&doc.model()))
                .and_then(|bytes| write_file(&output, &bytes))
            {
                Ok(()) => ExitCode::SUCCESS,
                Err(e) => {
                    eprintln!("{input}: {e}");
                    ExitCode::from(1)
                }
            }
        }
        #[cfg(feature = "render")]
        "to-pdf" => {
            let Some(input) = args.next() else {
                eprintln!(
                    "usage: rwml to-pdf <input.doc|.docx> <output.pdf> [--report-json report.json]"
                );
                return ExitCode::from(64);
            };
            let Some(output) = args.next() else {
                eprintln!(
                    "usage: rwml to-pdf <input.doc|.docx> <output.pdf> [--report-json report.json]"
                );
                return ExitCode::from(64);
            };
            let mut report_json = None;
            while let Some(arg) = args.next() {
                if arg == "--report-json" {
                    let Some(path) = args.next() else {
                        eprintln!("usage: rwml to-pdf <input.doc|.docx> <output.pdf> [--report-json report.json]");
                        return ExitCode::from(64);
                    };
                    report_json = Some(path);
                } else {
                    eprintln!("unknown to-pdf option {arg:?}");
                    return ExitCode::from(64);
                }
            }

            match render_pdf(&input, &output, report_json.as_deref()) {
                Ok(()) => ExitCode::SUCCESS,
                Err(e) => {
                    eprintln!("{input}: {e}");
                    ExitCode::from(1)
                }
            }
        }
        #[cfg(not(feature = "render"))]
        "to-pdf" => {
            eprintln!("rwml to-pdf requires the render feature");
            ExitCode::from(64)
        }
        "-h" | "--help" | "help" => {
            println!("{}", usage());
            ExitCode::SUCCESS
        }
        _ => {
            eprintln!("unknown command {command:?}\n{}", usage());
            ExitCode::from(64)
        }
    }
}

fn usage() -> &'static str {
    "usage: rwml <command> [args]\n\ncommands:\n  extract <file.doc|.docx>\n  diagnose <file.doc|.docx>\n  convert <file.doc|.docx> [txt|md|html]\n  to-docx <input.doc|.docx> <output.docx>\n  to-pdf <input.doc|.docx> <output.pdf> [--report-json report.json] (requires render)"
}

fn read_file(path: &str) -> rwml::Result<Vec<u8>> {
    std::fs::read(path).map_err(|e| rwml::Error::Docx(format!("read {path}: {e}")))
}

fn write_file(path: &str, bytes: &[u8]) -> rwml::Result<()> {
    std::fs::write(path, bytes).map_err(|e| rwml::Error::Docx(format!("write {path}: {e}")))
}

#[cfg(feature = "render")]
fn render_pdf(input: &str, output: &str, report_json: Option<&str>) -> rwml::Result<()> {
    let doc = read_file(input).and_then(|bytes| rwml::Document::open(&bytes))?;
    if let Some(report_path) = report_json {
        let rendered = doc.try_to_pdf_with_report()?;
        write_file(output, &rendered.pdf)?;
        let json = rendered.report.to_json();
        write_file(report_path, json.as_bytes())?;
    } else {
        let pdf = doc.try_to_pdf()?;
        write_file(output, &pdf)?;
    }
    Ok(())
}