1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Severity {
11 Error,
13 Warning,
16 Note,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Diagnostic {
24 pub severity: Severity,
26 pub origin: Option<String>,
29 pub message: String,
31 pub file: Option<String>,
33 pub line: Option<u32>,
35 pub source: Option<String>,
37 pub column: Option<usize>,
39 pub help: Vec<String>,
41 pub notes: Vec<String>,
43 pub facts: Vec<(String, String)>,
48 pub fixes: Vec<Fix>,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct Fix {
57 pub start: usize,
59 pub end: usize,
61 pub replacement: String,
63}
64
65impl Diagnostic {
66 pub fn new(severity: Severity, message: impl Into<String>) -> Self {
68 Diagnostic {
69 severity,
70 origin: None,
71 message: message.into(),
72 file: None,
73 line: None,
74 source: None,
75 column: None,
76 help: Vec::new(),
77 notes: Vec::new(),
78 facts: Vec::new(),
79 fixes: Vec::new(),
80 }
81 }
82
83 pub fn fact(&self, key: &str) -> Option<&str> {
85 self.facts
86 .iter()
87 .find(|(k, _)| k == key)
88 .map(|(_, v)| v.as_str())
89 }
90}
91
92pub fn edit_distance_within(a: &[u8], b: &[u8], max: usize) -> Option<usize> {
95 if a.len().abs_diff(b.len()) > max {
96 return None;
97 }
98 let mut prev: Vec<usize> = (0..=b.len()).collect();
99 for i in 1..=a.len() {
100 let mut cur = vec![i; b.len() + 1];
101 let mut row_min = cur[0];
102 for j in 1..=b.len() {
103 let cost = usize::from(a[i - 1] != b[j - 1]);
104 cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost);
105 row_min = row_min.min(cur[j]);
106 }
107 if row_min > max {
108 return None;
109 }
110 prev = cur;
111 }
112 (prev[b.len()] <= max).then_some(prev[b.len()])
113}
114
115pub fn parse_latex_warning(text: &str) -> Option<(String, String, Option<u32>)> {
121 let text = text.trim_start_matches('\n').trim_end();
122 let (head, rest) = text.split_once(" Warning: ")?;
123 let first_word = head.split(' ').next()?;
124 if !matches!(
125 first_word,
126 "LaTeX" | "Package" | "Class" | "pdfTeX" | "luatex"
127 ) {
128 return None;
129 }
130 if head.contains('\n') {
131 return None;
132 }
133 let name = head.rsplit(' ').next().unwrap_or(head);
135 let prefix = format!("({name})");
136 let mut parts: Vec<&str> = Vec::new();
137 for (i, l) in rest.lines().enumerate() {
138 let l = if i == 0 {
139 l
140 } else {
141 l.strip_prefix(prefix.as_str()).unwrap_or(l)
142 };
143 let l = l.trim();
144 if !l.is_empty() {
145 parts.push(l);
146 }
147 }
148 let mut message = parts.join(" ");
149 let mut line = None;
150 if let Some(i) = message.rfind(" on input line ") {
151 let num = message[i + 15..].trim_end_matches('.');
152 if let Ok(n) = num.parse() {
153 line = Some(n);
154 message.truncate(i);
155 message.push('.');
156 }
157 }
158 Some((head.to_owned(), message, line))
159}
160
161pub fn parse_latex_typed_error(text: &str) -> Option<(String, String)> {
166 let line = text.trim_start_matches('\n').lines().next()?.trim();
167 let rest = line.strip_prefix("! ")?;
168 let (head, message) = rest.split_once(" Error: ")?;
169 let first_word = head.split(' ').next()?;
170 if !matches!(first_word, "LaTeX" | "Package" | "Class") {
171 return None;
172 }
173 Some((head.to_owned(), message.trim().to_owned()))
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn latex_warnings() {
182 assert_eq!(
183 parse_latex_warning(
184 "\nLaTeX Warning: Reference `fig:x' on page 1 undefined on input line 12.\n"
185 ),
186 Some((
187 "LaTeX".to_owned(),
188 "Reference `fig:x' on page 1 undefined.".to_owned(),
189 Some(12)
190 ))
191 );
192 assert_eq!(
193 parse_latex_warning(
194 "\nPackage hyperref Warning: Token not allowed in a PDF string\n\
195 (hyperref) removing `math shift' on input line 5.\n"
196 ),
197 Some((
198 "Package hyperref".to_owned(),
199 "Token not allowed in a PDF string removing `math shift'.".to_owned(),
200 Some(5)
201 ))
202 );
203 assert_eq!(parse_latex_warning("Some other message"), None);
204 }
205
206 #[test]
207 fn latex_typed_errors() {
208 assert_eq!(
209 parse_latex_typed_error(
210 "\n! LaTeX Error: File `beamerthemex.sty' not found.\n\nType X to quit"
211 ),
212 Some((
213 "LaTeX".to_owned(),
214 "File `beamerthemex.sty' not found.".to_owned()
215 ))
216 );
217 assert_eq!(parse_latex_typed_error("LaTeX Warning: x"), None);
218 }
219}