1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! Structured diagnostics for retex's default output mode.
//!
//! In compatibility mode the terminal shows exactly what LuaTeX prints. In
//! the default mode the terminal is quiet and the engine records errors and
//! warnings here instead, with their source location; the command-line
//! driver presents them. The log file is the same in both modes.
/// How serious a diagnostic is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
/// A TeX error (the run continues after recovery, or stops if fatal).
Error,
/// A warning: overfull/underfull boxes, LaTeX and package warnings,
/// missing characters.
Warning,
}
/// One error or warning.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
/// Error or warning.
pub severity: Severity,
/// Who reports it, e.g. `LaTeX`, `Package hyperref`; `None` for the
/// engine itself.
pub origin: Option<String>,
/// The message, on one line.
pub message: String,
/// The input file, as TeX opened it.
pub file: Option<String>,
/// The line number in `file` (1-based).
pub line: Option<u32>,
/// The source line, if known.
pub source: Option<String>,
/// The 0-based character offset in `source` where reading stopped.
pub column: Option<usize>,
/// Help text.
pub help: Vec<String>,
/// Explanations of the cause, shown before the help.
pub notes: Vec<String>,
/// What the engine knew when the problem arose, for the explanations
/// of the default output mode: `similar` (defined commands close to an
/// undefined one), `token` (the token that caused a "Missing $"),
/// `math_line` (where the current formula began), …
pub facts: Vec<(String, String)>,
}
impl Diagnostic {
/// A diagnostic without location or help.
pub fn new(severity: Severity, message: impl Into<String>) -> Self {
Diagnostic {
severity,
origin: None,
message: message.into(),
file: None,
line: None,
source: None,
column: None,
help: Vec::new(),
notes: Vec::new(),
facts: Vec::new(),
}
}
/// The fact named `key`.
pub fn fact(&self, key: &str) -> Option<&str> {
self.facts
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
}
/// The Levenshtein distance between `a` and `b`, or `None` if it is more
/// than `max` (cheap to reject).
pub fn edit_distance_within(a: &[u8], b: &[u8], max: usize) -> Option<usize> {
if a.len().abs_diff(b.len()) > max {
return None;
}
let mut prev: Vec<usize> = (0..=b.len()).collect();
for i in 1..=a.len() {
let mut cur = vec![i; b.len() + 1];
let mut row_min = cur[0];
for j in 1..=b.len() {
let cost = usize::from(a[i - 1] != b[j - 1]);
cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost);
row_min = row_min.min(cur[j]);
}
if row_min > max {
return None;
}
prev = cur;
}
(prev[b.len()] <= max).then_some(prev[b.len()])
}
/// Parses a LaTeX-style warning written to the terminal, such as
/// `LaTeX Warning: Reference `x' on page 1 undefined on input line 12.` or
/// `Package hyperref Warning: Token not allowed ... (hyperref) removing ...`.
/// Returns the origin, the message (continuation prefixes removed, lines
/// joined) and the input line it names, if any.
pub fn parse_latex_warning(text: &str) -> Option<(String, String, Option<u32>)> {
let text = text.trim_start_matches('\n').trim_end();
let (head, rest) = text.split_once(" Warning: ")?;
let first_word = head.split(' ').next()?;
if !matches!(
first_word,
"LaTeX" | "Package" | "Class" | "pdfTeX" | "luatex"
) {
return None;
}
if head.contains('\n') {
return None;
}
// Continuation lines start with "(name)" and spaces.
let name = head.rsplit(' ').next().unwrap_or(head);
let prefix = format!("({name})");
let mut parts: Vec<&str> = Vec::new();
for (i, l) in rest.lines().enumerate() {
let l = if i == 0 {
l
} else {
l.strip_prefix(prefix.as_str()).unwrap_or(l)
};
let l = l.trim();
if !l.is_empty() {
parts.push(l);
}
}
let mut message = parts.join(" ");
let mut line = None;
if let Some(i) = message.rfind(" on input line ") {
let num = message[i + 15..].trim_end_matches('.');
if let Ok(n) = num.parse() {
line = Some(n);
message.truncate(i);
message.push('.');
}
}
Some((head.to_owned(), message, line))
}
/// Parses an error that LaTeX writes to the terminal instead of raising
/// it with `\errmessage`: `! LaTeX Error: File `x.sty' not found.` and
/// the like (`! Package P Error: …`, `! Class C Error: …`). Returns the
/// origin (`LaTeX`, `Package P`, …) and the first line of the message.
pub fn parse_latex_typed_error(text: &str) -> Option<(String, String)> {
let line = text.trim_start_matches('\n').lines().next()?.trim();
let rest = line.strip_prefix("! ")?;
let (head, message) = rest.split_once(" Error: ")?;
let first_word = head.split(' ').next()?;
if !matches!(first_word, "LaTeX" | "Package" | "Class") {
return None;
}
Some((head.to_owned(), message.trim().to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latex_warnings() {
assert_eq!(
parse_latex_warning(
"\nLaTeX Warning: Reference `fig:x' on page 1 undefined on input line 12.\n"
),
Some((
"LaTeX".to_owned(),
"Reference `fig:x' on page 1 undefined.".to_owned(),
Some(12)
))
);
assert_eq!(
parse_latex_warning(
"\nPackage hyperref Warning: Token not allowed in a PDF string\n\
(hyperref) removing `math shift' on input line 5.\n"
),
Some((
"Package hyperref".to_owned(),
"Token not allowed in a PDF string removing `math shift'.".to_owned(),
Some(5)
))
);
assert_eq!(parse_latex_warning("Some other message"), None);
}
#[test]
fn latex_typed_errors() {
assert_eq!(
parse_latex_typed_error(
"\n! LaTeX Error: File `beamerthemex.sty' not found.\n\nType X to quit"
),
Some((
"LaTeX".to_owned(),
"File `beamerthemex.sty' not found.".to_owned()
))
);
assert_eq!(parse_latex_typed_error("LaTeX Warning: x"), None);
}
}