latexdef/
run.rs

1use std::io;
2use std::process::ExitStatus;
3use std::str::Lines;
4use std::string::FromUtf8Error;
5
6use ansi_term::Color::Black;
7use lazy_static::lazy_static;
8use quick_error::quick_error;
9use regex::Regex;
10
11lazy_static! {
12    static ref AUX_FILE: Regex = {
13        // Lines like:
14        //      *(/tmp/texput.aux)
15        Regex::new(r"^\*\(([^)]+\.aux)\)$").unwrap()
16    };
17
18    static ref LOG_FILE: Regex = {
19        // Lines like:
20        //      Transcript written on /tmp/texput.log.
21        Regex::new(r"^Transcript written on (.+\.log).$").unwrap()
22    };
23
24    static ref MACRO_NAME: Regex = {
25        Regex::new(r"^\*> (\\.|\\[a-zA-Z:_@]+)=(.*)$").unwrap()
26    };
27
28    static ref MACRO_PARAMS: Regex = {
29        Regex::new(r"^((?:#\d)*)->").unwrap()
30    };
31}
32
33pub fn first_group<'a>(re: &Regex, line: &'a str) -> Option<&'a str> {
34    re.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
35}
36
37pub fn aux_file(line: &str) -> Option<&str> {
38    first_group(&AUX_FILE, line)
39}
40
41pub fn log_file(line: &str) -> Option<&str> {
42    first_group(&LOG_FILE, line)
43}
44
45quick_error! {
46    #[derive(Debug)]
47    pub enum RunError {
48        FailedToStart(engine: String) {
49            display("Failed to start TeX engine {:?}.", engine)
50        }
51
52        TexFailed(status: ExitStatus) {
53            display("TeX engine didn't complete successfully; exit status {:?}.", status)
54        }
55
56        TexStderr(err: String) {
57            display("TeX wrote to stderr: {}", err)
58        }
59
60        NoPipe {
61            display("Didn't capture stdin/stdout/stderr pipe.")
62        }
63
64        FromUtf8(err: FromUtf8Error) {}
65
66        Io(err: io::Error) {}
67    }
68}
69
70#[derive(PartialEq, Debug)]
71enum LineKind {
72    Ignore,
73    MacroName,
74    MacroStart,
75    MacroContinue,
76}
77
78impl Default for LineKind {
79    fn default() -> Self {
80        LineKind::Ignore
81    }
82}
83
84#[derive(Default, Debug, Clone)]
85pub struct TexCommand {
86    // The name, including the backslash.
87    pub name: String,
88    pub kind: String,
89    // The parameters, i.e. #1#2#3
90    pub parameters: String,
91    // The definition, or expansion text.
92    pub definition: String,
93}
94
95impl TexCommand {
96    pub fn is_undefined(&self) -> bool {
97        self.definition.is_empty() && (self.kind == "\\relax." && self.name != "\\relax")
98    }
99
100    pub fn is_primitive(&self) -> bool {
101        self.name == self.kind.trim_end_matches('.')
102    }
103
104    pub fn has_parameters(&self) -> bool {
105        !self.parameters.is_empty()
106    }
107}
108
109#[derive(Debug)]
110pub struct LatexJob<'a> {
111    lines: Lines<'a>,
112    line: LineKind,
113    current: TexCommand,
114    verbose: bool,
115}
116
117impl<'a> LatexJob<'a> {
118    pub fn new(s: &'a str) -> Self {
119        LatexJob {
120            lines: s.lines(),
121            line: LineKind::Ignore,
122            current: TexCommand::default(),
123            verbose: false,
124        }
125    }
126
127    pub fn verbose(&mut self, is_verbose: bool) -> &mut Self {
128        self.verbose = is_verbose;
129        self
130    }
131}
132
133impl<'a> Iterator for LatexJob<'a> {
134    type Item = TexCommand;
135
136    fn next(&mut self) -> Option<Self::Item> {
137        let line = self.lines.next()?;
138        if self.verbose {
139            println!("{}", Black.bold().paint(line));
140        }
141        if line.starts_with("<recently read>") {
142            // Finished with this definition, return.
143            self.line = LineKind::Ignore;
144            let last_char = self.current.definition.pop();
145            match last_char {
146                Some('.') => {}
147                Some(c) => {
148                    panic!(
149                        "Expected to find '.' as last char of definition, instead found '{:?}'.\n{:?}",
150                        c, self.current
151                    );
152                }
153                None => {}
154            }
155            let ret = self.current.clone();
156            self.current = TexCommand::default();
157            Some(ret)
158        } else if let Some(caps) = MACRO_NAME.captures(line) {
159            // Starting a new definition.
160            self.line = LineKind::MacroName;
161            self.current = TexCommand {
162                name: caps.get(1).unwrap().as_str().into(),
163                kind: caps.get(2).unwrap().as_str().trim_end_matches(':').into(),
164                ..Default::default()
165            };
166            self.next()
167        } else if let Some(i) = line.find("->") {
168            // Parameters; first line of a new definition.
169            self.line = LineKind::MacroStart;
170            let (params, rest) = line.split_at(i);
171            self.current.parameters = params.into();
172            self.current.definition.push_str(rest.get(2..).unwrap());
173            self.next()
174        } else {
175            match self.line {
176                LineKind::Ignore => {}
177                LineKind::MacroName => {
178                    panic!(
179                        "Impossible state; should have found macro parameters! {:?}\nLine: {:?}",
180                        self.current, line
181                    );
182                }
183                LineKind::MacroStart => {
184                    // Just past the parameters line.
185                    self.line = LineKind::MacroContinue;
186                    self.current.definition.push_str(line);
187                }
188                LineKind::MacroContinue => {
189                    self.current.definition.push_str(line);
190                }
191            }
192            self.next()
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn macro_name_test() {
203        assert!(MACRO_NAME.is_match("*> \\@author=\\macro:"));
204        assert!(MACRO_NAME.is_match("*> \\parskip=\\parskip."));
205
206        let caps = MACRO_NAME.captures("*> \\bool_new:N=\\macro:").unwrap();
207        assert_eq!(caps.get(1).unwrap().as_str(), "\\bool_new:N");
208        assert_eq!(caps.get(2).unwrap().as_str(), "\\macro:");
209    }
210
211    #[test]
212    fn macro_params_test() {
213        assert!(MACRO_PARAMS.is_match("#1->xyz..."));
214        assert!(MACRO_PARAMS.is_match("#1#2#3->xyz..."));
215        assert!(MACRO_PARAMS.is_match("-> zoooop"));
216        assert!(!MACRO_PARAMS.is_match("#10-> zoooop"));
217    }
218}