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 Regex::new(r"^\*\(([^)]+\.aux)\)$").unwrap()
16 };
17
18 static ref LOG_FILE: Regex = {
19 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 pub name: String,
88 pub kind: String,
89 pub parameters: String,
91 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 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 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 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 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}