lib/
pandoc.rs

1/// This module contains all functions to call pandoc and handle any errors occurring mine while.
2use crate::metadata::Metadata;
3
4use std::env;
5use std::fmt;
6use std::io::{self, Error as IOError, ErrorKind};
7use std::path::PathBuf;
8use std::process::{Command, Output};
9
10/// Default name of the pandoc executable. Will be used when no other name is defined via the
11/// `PANDOC_ENV` constant of this module.
12const PANDOC_CMD: &str = "pandoc";
13
14/// Name of the environment variable which will be used to determine the name of the pandoc
15/// executable.
16const PANDOC_ENV: &str = "PANDOC_CMD";
17
18/// Contains information about the calling of the pandoc command. Used to accompany error messages
19/// when the pandoc execution fails.
20pub struct DebugInfo {
21    /// Source file.
22    input: String,
23    /// Path to the template file.
24    template: Option<String>,
25    /// Output file.
26    output: String,
27    /// Pandoc stderr.
28    err: String,
29    /// States whether pandoc was called for extracting the metadata in the header or not. This
30    /// alters the error message.
31    tried_extracting_header: bool,
32}
33
34impl fmt::Display for DebugInfo {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        match self.tried_extracting_header {
37            true => write!(
38                f,
39                "pandoc failed to extract header metadata from \"{}\" {}",
40                self.input, self.err,
41            ),
42
43            false => write!(
44                f,
45                "pandoc failed to convert \"{}\" to \"{}\" with template \"{}\" {}",
46                self.input,
47                self.output,
48                match self.template {
49                    Some(ref x) => String::from(x),
50                    None => String::from("<undefined>"),
51                },
52                self.err,
53            ),
54        }
55    }
56}
57
58/// Defines the different kinds of pandoc errors.
59pub enum PandocError<'a> {
60    /// The pandoc executable wasn't found on the system. Contains the used name for the pandoc
61    /// executable.
62    NotFound(String),
63    /// The methods of Pandoc a instance only accept absolute paths as argument. Hereby the current
64    /// working directory cannot infer with the execution of pandoc. All public Pandoc methods have
65    /// to check if a path argument is absolute. Contains the erroneous path and a description of
66    /// it's purpose.
67    RelativePath(PathBuf, &'a str),
68    /// Couldn't convert the Vec<u8> from the pandoc stdout to a string.
69    StringFromUtf8,
70    /// The execution of pandoc failed. Contains DebugInfo structure.
71    ExecutionFailed(DebugInfo),
72    /// Couldn't call the pandoc command but the executable is on the system with the used name,
73    /// thus not a NotFound error.
74    CallFailed(IOError),
75}
76
77impl fmt::Display for PandocError<'_> {
78    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        match &self {
80            PandocError::NotFound(executable) => match executable == &PANDOC_CMD {
81                true => write!(
82                    f,
83                    "couldn't find \"pandoc\" on your system, use the env \"{}\" to use an non default executable name",
84                    PANDOC_ENV
85                ),
86                false => write!(
87                    f,
88                    "couldn't find pandoc with the executable name \"{}\" use env \"{}\" to specify otherwise",
89                    executable,
90                    PANDOC_ENV
91                ),
92            },
93            PandocError::RelativePath(path, purpose) => write!(
94                f,
95                "internal error, pandoc module was called with an relative {} path {}, only absolute paths allowed",
96                purpose,
97                path.display()
98            ),
99            PandocError::StringFromUtf8 => write!(
100                f,
101                "couldn't convert standard output (stdout) from pandoc"
102            ),
103            PandocError::ExecutionFailed(info) => write!(
104                f,
105                "{}",
106                info
107            ),
108            PandocError::CallFailed(err) => write!(
109                f,
110                "couldn't call pandoc {}",
111                err,
112            ),
113        }
114    }
115}
116
117/// Wrapper for calling pandoc. Exposes all needed functionality via it's method. Contains the
118/// executable name for pandoc.
119pub struct Pandoc(String);
120
121impl<'a> Pandoc {
122    /// Returns a new instance of the Pandoc struct. Will use the `PANDOC_CMD` environment variable
123    /// to determine the name of the pandoc executable. If the variable isn't set the constant
124    /// PANDOC_CMD will be used.
125    pub fn new() -> Self {
126        Self(match env::var(PANDOC_ENV) {
127            Ok(x) => x,
128            Err(_) => String::from(PANDOC_CMD),
129        })
130    }
131
132    /// Converts a given file with a template and returns the result as a string. This function is
133    /// mainly used for the extraction of the frontmatter header as a JSON file.
134    pub fn convert_with_template_to_str(
135        &self,
136        input: &PathBuf,
137        template: PathBuf,
138    ) -> Result<String, PandocError<'a>> {
139        check_path(input.clone(), "input")?;
140        check_path(template.clone(), "template")?;
141        debug!(
142            "input: {}, template: {}",
143            input.display(),
144            template.display()
145        );
146
147        let mut cmd = Command::new(self.0.clone());
148        cmd.arg("--template").arg(template).arg(&input);
149
150        Pandoc::output_to_result(
151            cmd.output(),
152            self.0.clone(),
153            true,
154            String::from(input.to_str().unwrap()),
155            String::new(),
156            None,
157        )
158    }
159
160    /// Converts a given file with a template to a PDF. Optionally it's possible to add
161    /// parameters to the pandoc call. The resource_path parameter can optionally state the folder
162    /// path to which the links within the document (images etc.) are relative to. This way the
163    /// conversion can happen in the temporary folder while correctly referencing the relative
164    /// embedded links in the markdown document.
165    pub fn convert_with_metadata_to_pdf(
166        &self,
167        input: &PathBuf,
168        metadata: Metadata,
169        output: &PathBuf,
170        resource_path: Option<&PathBuf>,
171    ) -> Result<(), PandocError<'a>> {
172        let mut cmd = Command::new(self.0.clone());
173        cmd.arg(&input)
174            .arg("--pdf-engine")
175            .arg(metadata.engine)
176            .arg("--wrap=preserve");
177        if let Some(ref template) = metadata.template {
178            cmd.arg("--template").arg(template);
179        }
180        if let Some(options) = metadata.pandoc_options {
181            cmd.args(options);
182        }
183        if let Some(_bibliography) = metadata.bibliography {
184            cmd.arg("--citeproc");
185        }
186        if let Some(path) = resource_path {
187            cmd.arg("--resource-path").arg(path);
188        }
189        cmd.arg("-o").arg(&output);
190        match Pandoc::output_to_result(
191            cmd.output(),
192            self.0.clone(),
193            false,
194            String::from(input.to_str().unwrap()),
195            String::from(output.to_str().unwrap()),
196            match metadata.template {
197                Some(x) => Some(String::from(x.to_str().unwrap())),
198                None => None,
199            },
200        ) {
201            Ok(_) => Ok(()),
202            Err(e) => Err(e),
203        }
204    }
205
206    /// Converts a given file with a template to a OpenDocument text or word file. Optionally
207    /// it's possible to add parameters to the pandoc call. The resource_path parameter can
208    /// optionally state the folder path to which the links within the document (images etc.)
209    /// are relative to. This way the conversion can happen in the temporary folder while
210    /// correctly referencing the relative embedded links in the markdown document.
211    pub fn convert_with_metadata_to_office(
212        &self,
213        input: &PathBuf,
214        metadata: Metadata,
215        output: &PathBuf,
216        resource_path: Option<&PathBuf>,
217    ) -> Result<(), PandocError<'a>> {
218        let mut cmd = Command::new(self.0.clone());
219        cmd.arg(&input);
220        if let Some(ref reference) = metadata.reference {
221            cmd.arg("--reference-doc").arg(reference);
222        }
223        if let Some(options) = metadata.pandoc_options {
224            cmd.args(options);
225        }
226        if let Some(_bibliography) = metadata.bibliography {
227            cmd.arg("--citeproc");
228        }
229        if let Some(path) = resource_path {
230            cmd.arg("--resource-path").arg(path);
231        }
232        cmd.arg("-o").arg(&output);
233        match Pandoc::output_to_result(
234            cmd.output(),
235            self.0.clone(),
236            false,
237            String::from(input.to_str().unwrap()),
238            String::from(output.to_str().unwrap()),
239            None,
240        ) {
241            Ok(_) => Ok(()),
242            Err(e) => Err(e),
243        }
244    }
245
246    /// Converts a given file with a template to a Reveal.js presentation. Optionally it's possible
247    /// to add parameters to the pandoc call. The resource_path parameter can optionally state the
248    /// folder path to which the links within the document (images etc.) are relative to. This way the
249    /// conversion can happen in the temporary folder while correctly referencing the relative
250    /// embedded links in the markdown document.
251    pub fn convert_with_metadata_to_reveal(
252        &self,
253        input: &PathBuf,
254        metadata: Metadata,
255        output: &PathBuf,
256        resource_path: Option<&PathBuf>,
257    ) -> Result<(), PandocError<'a>> {
258        let mut cmd = Command::new(self.0.clone());
259        cmd.arg(&input)
260            .arg("-t")
261            .arg("revealjs")
262            .arg("-s");
263        if let Some(options) = metadata.pandoc_options {
264            cmd.args(options);
265        }
266        if let Some(_bibliography) = metadata.bibliography {
267            cmd.arg("--citeproc");
268        }
269        if let Some(path) = resource_path {
270            cmd.arg("--resource-path").arg(path);
271        }
272        cmd.arg("-o").arg(&output);
273        match Pandoc::output_to_result(
274            cmd.output(),
275            self.0.clone(),
276            false,
277            String::from(input.to_str().unwrap()),
278            String::from(output.to_str().unwrap()),
279            None,
280        ) {
281            Ok(_) => Ok(()),
282            Err(e) => Err(e),
283        }
284    }
285
286
287    /// Checks the output of a pandoc call and returns the appropriate result.
288    fn output_to_result(
289        rsl: io::Result<Output>,
290        pandoc_bin: String,
291        tried_extracting_header: bool,
292        input: String,
293        output: String,
294        temlate: Option<String>,
295    ) -> Result<String, PandocError<'a>> {
296        match rsl {
297            Ok(x) => {
298                if x.status.success() {
299                    match String::from_utf8(x.stdout) {
300                        Ok(x) => Ok(x),
301                        Err(_) => Err(PandocError::StringFromUtf8),
302                    }
303                } else {
304                    Err(PandocError::ExecutionFailed(DebugInfo {
305                        input: input,
306                        output: output,
307                        template: temlate,
308                        err: String::from_utf8(x.stderr).unwrap(),
309                        tried_extracting_header: tried_extracting_header,
310                    }))
311                }
312            }
313            Err(e) => {
314                if let ErrorKind::NotFound = e.kind() {
315                    Err(PandocError::NotFound(pandoc_bin))
316                } else {
317                    Err(PandocError::CallFailed(e))
318                }
319            }
320        }
321    }
322}
323
324/// Checks if the given path is absolute and returns the corresponding PandocError.
325fn check_path<'a>(path: PathBuf, purpose: &'a str) -> Result<(), PandocError<'a>> {
326    match path.is_absolute() {
327        true => Ok(()),
328        false => Err(PandocError::RelativePath(path, purpose)),
329    }
330}