Skip to main content

canonic/
convert.rs

1//! Pandoc-backed markdown → Jira/Confluence wiki markup conversion.
2
3use anyhow::{bail, Context, Result};
4use std::path::Path;
5use std::process::Command;
6
7/// First line of `pandoc --version` when available.
8pub fn pandoc_version_line() -> Option<String> {
9    let out = Command::new("pandoc").arg("--version").output().ok()?;
10    if !out.status.success() && out.stdout.is_empty() {
11        return None;
12    }
13    String::from_utf8_lossy(&out.stdout)
14        .lines()
15        .next()
16        .map(|s| s.trim().to_string())
17        .filter(|s| !s.is_empty())
18}
19
20/// Check whether `pandoc` is available on PATH.
21pub fn tool_available() -> bool {
22    Command::new("pandoc")
23        .arg("--version")
24        .output()
25        .map(|o| o.status.success())
26        .unwrap_or(false)
27}
28
29fn run_pandoc(from: &str, to: &str, input: &str) -> Result<String> {
30    if !tool_available() {
31        bail!(
32            "pandoc is not installed or not on PATH; install pandoc to convert between markdown and jira markup"
33        );
34    }
35    let mut child = Command::new("pandoc")
36        .args(["-f", from, "-t", to])
37        .stdin(std::process::Stdio::piped())
38        .stdout(std::process::Stdio::piped())
39        .stderr(std::process::Stdio::piped())
40        .spawn()
41        .context("spawn pandoc")?;
42
43    use std::io::Write;
44    {
45        let mut stdin = child.stdin.take().context("open pandoc stdin")?;
46        stdin
47            .write_all(input.as_bytes())
48            .context("write input to pandoc")?;
49    }
50
51    let output = child.wait_with_output().context("wait for pandoc")?;
52    if !output.status.success() {
53        let err = String::from_utf8_lossy(&output.stderr);
54        bail!("pandoc failed (status {}): {err}", output.status);
55    }
56    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
57}
58
59/// Convert markdown text to Jira wiki markup via `pandoc -f markdown -t jira`.
60pub fn convert_markdown_to_jira(markdown: &str) -> Result<String> {
61    run_pandoc("markdown", "jira", markdown)
62}
63
64/// Convert Jira wiki markup back to markdown via `pandoc -f jira -t markdown`
65/// (the reverse of [`convert_markdown_to_jira`]; used by the Jira import read path).
66pub fn convert_jira_to_markdown(jira: &str) -> Result<String> {
67    run_pandoc("jira", "markdown", jira)
68}
69
70/// Read a markdown file and convert to Jira markup.
71pub fn convert_path_to_jira(path: &Path) -> Result<String> {
72    let md = std::fs::read_to_string(path)
73        .with_context(|| format!("read {}", path.display()))?;
74    convert_markdown_to_jira(&md)
75}
76
77/// Build the pandoc argv used for conversion (for unit tests of invocation construction).
78pub fn pandoc_jira_args() -> Vec<&'static str> {
79    vec!["-f", "markdown", "-t", "jira"]
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn pandoc_args_target_jira_writer() {
88        let args = pandoc_jira_args();
89        assert!(args.contains(&"markdown"));
90        assert!(args.contains(&"jira"));
91        assert_eq!(args, vec!["-f", "markdown", "-t", "jira"]);
92    }
93
94    #[test]
95    fn convert_sample_when_pandoc_present() {
96        if !tool_available() {
97            // Explicit path still exists; missing tool is reported by convert_markdown_to_jira.
98            let err = convert_markdown_to_jira("# Hi").unwrap_err().to_string();
99            assert!(err.to_lowercase().contains("pandoc"));
100            return;
101        }
102        let jira = convert_markdown_to_jira(
103            "# Password reset\n\nUse **self-service** portal:\n\n- step one\n- step two\n",
104        )
105        .unwrap();
106        // Pandoc jira writer: headings as h1., bold as *text*, lists as * or -
107        assert!(
108            jira.contains("h1.") || jira.contains("Password"),
109            "expected jira heading or title, got: {jira:?}"
110        );
111        assert!(
112            jira.contains("*self-service*") || jira.contains("self-service"),
113            "expected bold or body text, got: {jira:?}"
114        );
115    }
116
117    #[test]
118    fn convert_jira_to_markdown_reverses_the_writer() {
119        if !tool_available() {
120            let err = convert_jira_to_markdown("h1. Hi").unwrap_err().to_string();
121            assert!(err.to_lowercase().contains("pandoc"));
122            return;
123        }
124        let markdown =
125            convert_jira_to_markdown("h1. Password reset\n\nUse *self-service* portal.\n")
126                .unwrap();
127        assert!(markdown.contains("# Password reset"), "{markdown:?}");
128        assert!(markdown.contains("**self-service**"), "{markdown:?}");
129    }
130}