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
//! 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>,
}
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(),
}
}
}
/// 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))
}
#[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);
}
}