snapper_fmt/parser/pandoc/
cli.rs1use std::io::Write;
8use std::process::{Command, Stdio};
9
10use thiserror::Error;
11
12use super::ast::regions_from_pandoc_json;
13use crate::parser::Region;
14
15#[derive(Debug, Error)]
16pub enum CliError {
17 #[error("pandoc CLI unavailable or failed to start: {0}")]
18 Spawn(String),
19 #[error("pandoc CLI exited with failure: {0}")]
20 Exit(String),
21 #[error("pandoc CLI returned invalid AST: {0}")]
22 InvalidAst(String),
23}
24
25pub fn pandoc_cli_available() -> bool {
27 Command::new("pandoc")
28 .arg("--version")
29 .stdout(Stdio::null())
30 .stderr(Stdio::null())
31 .status()
32 .is_ok_and(|s| s.success())
33}
34
35pub fn parse_via_cli(input: &str, format: &str) -> Result<Vec<Region>, CliError> {
37 let (regions, _) = parse_via_cli_with_json(input, format)?;
38 Ok(regions)
39}
40
41pub fn parse_via_cli_with_json(
43 input: &str,
44 format: &str,
45) -> Result<(Vec<Region>, String), CliError> {
46 let mut child = Command::new("pandoc")
47 .args(["-f", format, "-t", "json"])
48 .stdin(Stdio::piped())
49 .stdout(Stdio::piped())
50 .stderr(Stdio::piped())
51 .spawn()
52 .map_err(|e| CliError::Spawn(e.to_string()))?;
53
54 if let Some(ref mut stdin) = child.stdin {
55 stdin
56 .write_all(input.as_bytes())
57 .map_err(|e| CliError::Spawn(format!("write stdin: {e}")))?;
58 }
59
60 let output = child
61 .wait_with_output()
62 .map_err(|e| CliError::Spawn(e.to_string()))?;
63
64 if !output.status.success() {
65 let stderr = String::from_utf8_lossy(&output.stderr);
66 return Err(CliError::Exit(format!(
67 "status {}: {stderr}",
68 output.status
69 )));
70 }
71
72 let json = String::from_utf8(output.stdout)
73 .map_err(|e| CliError::InvalidAst(format!("stdout not UTF-8: {e}")))?;
74 let regions = regions_from_pandoc_json(&json).map_err(CliError::InvalidAst)?;
75 Ok((regions, json))
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn cli_availability_check_does_not_panic() {
84 let _ = pandoc_cli_available();
85 }
86
87 #[test]
88 fn cli_unknown_format_is_explicit_error() {
89 if !pandoc_cli_available() {
90 return;
91 }
92 let err = parse_via_cli("Hello.", "not-a-real-pandoc-format-xyz").unwrap_err();
93 match err {
94 CliError::Exit(_) | CliError::Spawn(_) | CliError::InvalidAst(_) => {}
95 }
96 }
97}