use std::{cmp::Ordering, path::PathBuf}; use once_cell::sync::Lazy; use regex::{Match, Regex}; use syntax::{BuildError, BuildErrorLevel, BuildLog}; const MAX_LINE_LENGTH: usize = 79; static PACKAGE_MESSAGE_REGEX: Lazy = Lazy::new(|| Regex::new("^\\([a-zA-Z_\\-]+\\)\\s*(?P.*)$").unwrap()); static FILE_REGEX: Lazy = Lazy::new(|| Regex::new("\\((?P[^\r\n()]+\\.(tex|sty|cls))").unwrap()); static TEX_ERROR_REGEX: Lazy = Lazy::new(|| { Regex::new("(?m)^! ((?P(.|\r|\n)*?)\r?\nl\\.(?P\\d+)( (\\.\\.\\.)?(?P[^\r\n]+))?|(?P[^\r\n]*))").unwrap() }); static WARNING_REGEX: Lazy = Lazy::new(|| { Regex::new("(?P(LaTeX|Package [a-zA-Z_\\-]+) Warning: [^\r\n]*?(on input line (?P\\d+))?\\.)[\r\n]").unwrap() }); static BAD_BOX_REGEX: Lazy = Lazy::new(|| { Regex::new("(?P(Ov|Und)erfull \\\\[hv]box[^\r\n]*lines? (?P\\d+)[^\r\n]*)").unwrap() }); pub fn parse_build_log(log: &str) -> BuildLog { let log = prepare_log(log); let mut ranges: Vec = FILE_REGEX .find_iter(&log) .map(|result| FileRange::create(&log, result)) .collect(); ranges.sort(); let tex_errors = extract_matches(&log, &ranges, &TEX_ERROR_REGEX, BuildErrorLevel::Error); let warnings = extract_matches(&log, &ranges, &WARNING_REGEX, BuildErrorLevel::Warning); let bad_boxes = extract_matches(&log, &ranges, &BAD_BOX_REGEX, BuildErrorLevel::Warning); let errors = [tex_errors, warnings, bad_boxes].concat(); BuildLog { errors } } fn extract_matches( log: &str, ranges: &[FileRange], regex: &Regex, level: BuildErrorLevel, ) -> Vec { let mut errors = Vec::new(); for result in regex.find_iter(log) { let captures = regex.captures(&log[result.start()..result.end()]).unwrap(); let message = captures .name("msg") .or_else(|| captures.name("msg1")) .or_else(|| captures.name("msg2")) .unwrap() .as_str() .lines() .next() .unwrap_or_default() .to_owned(); if let Some(range) = ranges.iter().find(|range| range.contains(result.start())) { let line = captures .name("line") .map(|result| result.as_str().parse::().unwrap() - 1); let hint: Option = if line.is_some() { captures .name("hint") .map(|r| String::from(r.as_str().trim())) } else { None }; errors.push(BuildError { relative_path: range.path.clone(), level, message, line, hint, }); } } errors } fn prepare_log(log: &str) -> String { let mut old_lines = log.lines(); let mut new_lines: Vec = Vec::new(); while let Some(line) = old_lines.next() { if PACKAGE_MESSAGE_REGEX.is_match(line) { let captures = PACKAGE_MESSAGE_REGEX.captures(line).unwrap(); if let Some(last_line) = new_lines.last_mut() { last_line.push(' '); last_line.push_str(captures.name("msg").unwrap().as_str()); } } else if line.ends_with("...") { let mut new_line = line[line.len() - 3..].to_owned(); if let Some(old_line) = old_lines.next() { new_line.push_str(old_line); } new_lines.push(new_line); } else if line.chars().count() == MAX_LINE_LENGTH { let mut new_line = String::new(); new_line.push_str(line); for old_line in old_lines.by_ref() { new_line.push_str(old_line); if old_line.chars().count() != MAX_LINE_LENGTH { break; } } new_lines.push(new_line); } else { new_lines.push(line.to_owned()); } } new_lines.join("\n") } #[derive(Debug, PartialEq, Eq, Clone)] struct FileRange { pub path: PathBuf, pub start: usize, pub end: usize, } impl FileRange { fn create(log: &str, result: Match) -> Self { let mut balance = 1; let mut end = result.start() + 1; let chars = log[result.start() + 1..].chars(); for c in chars { if balance <= 0 { break; } if c == '(' { balance += 1; } else if c == ')' { balance -= 1; } end += c.len_utf8(); } let captures = FILE_REGEX.captures(result.as_str()).unwrap(); let path = PathBuf::from(captures.name("file").unwrap().as_str()); Self { path, start: result.start(), end, } } fn len(&self) -> usize { self.end - self.start + 1 } fn contains(&self, index: usize) -> bool { index >= self.start && index <= self.end } } impl Ord for FileRange { fn cmp(&self, other: &Self) -> Ordering { self.len().cmp(&other.len()) } } impl PartialOrd for FileRange { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } #[cfg(test)] mod tests;