Skip to main content

clayers_xml/
xslt.rs

1use std::fs;
2use std::process::Command;
3
4use crate::error::Error;
5
6/// Transform XML using a set of XSLT files.
7///
8/// The first entry in `xslt_files` is the main stylesheet (entry point);
9/// the rest are written alongside it so that `xsl:import href="foo.xslt"`
10/// resolves relative to the same directory.
11///
12/// Returns the transformed output as a string.
13///
14/// # Errors
15///
16/// Returns `Error::Xslt` if Saxon is not found, fails to run, or
17/// produces non-zero exit status.
18pub fn transform(xml: &str, xslt_files: &[(&str, &str)]) -> Result<String, Error> {
19    let tmp_dir = tempfile::tempdir().map_err(|e| Error::Xslt(format!("tempdir: {e}")))?;
20
21    // Write all XSLT files into the temp directory.
22    for (name, content) in xslt_files {
23        fs::write(tmp_dir.path().join(name), content)?;
24    }
25
26    // Write the XML input.
27    let xml_path = tmp_dir.path().join("input.xml");
28    fs::write(&xml_path, xml)?;
29
30    // Determine the main stylesheet (first entry).
31    let main_xslt = xslt_files
32        .first()
33        .map(|(name, _)| name)
34        .ok_or_else(|| Error::Xslt("no XSLT files provided".into()))?;
35    let xslt_path = tmp_dir.path().join(main_xslt);
36
37    // Find saxon.
38    let saxon = find_saxon()?;
39
40    // Run saxon.
41    let output = Command::new(&saxon)
42        .arg(format!("-s:{}", xml_path.display()))
43        .arg(format!("-xsl:{}", xslt_path.display()))
44        .output()
45        .map_err(|e| Error::Xslt(format!("failed to run saxon: {e}")))?;
46
47    if !output.status.success() {
48        let stderr = String::from_utf8_lossy(&output.stderr);
49        return Err(Error::Xslt(format!("saxon failed: {stderr}")));
50    }
51
52    String::from_utf8(output.stdout)
53        .map_err(|e| Error::Xslt(format!("saxon output is not UTF-8: {e}")))
54}
55
56fn find_saxon() -> Result<String, Error> {
57    // Check PATH for `saxon`.
58    let check = Command::new("which").arg("saxon").output();
59    if let Ok(output) = check
60        && output.status.success()
61    {
62        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
63        if path.is_empty() {
64            return Err(Error::Xslt(install_instructions()));
65        }
66        return Ok(path);
67    }
68
69    Err(Error::Xslt(install_instructions()))
70}
71
72fn install_instructions() -> String {
73    let os = std::env::consts::OS;
74    let mut msg = String::from("saxon not found in PATH.\n\nInstall instructions:\n");
75
76    match os {
77        "macos" => {
78            msg.push_str("  brew install saxon\n");
79        }
80        "linux" => {
81            msg.push_str(
82                "  Download Saxon-HE from:\n  \
83                 https://github.com/Saxonica/Saxon-HE/releases\n  \
84                 Then add the `saxon` wrapper script to your PATH.\n",
85            );
86        }
87        "windows" => {
88            msg.push_str(
89                "  Download Saxon-HE from:\n  \
90                 https://github.com/Saxonica/Saxon-HE/releases\n  \
91                 Then add saxon.exe to your PATH.\n",
92            );
93        }
94        _ => {
95            msg.push_str(
96                "  Download Saxon-HE from:\n  \
97                 https://github.com/Saxonica/Saxon-HE/releases\n",
98            );
99        }
100    }
101
102    msg
103}