retex-engine 0.9.0

The TeX typesetting engine for retex
Documentation
//! 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);
    }
}