pulldown_latex/parser/
error.rs

1//! Error type returned by the parser upon failure.
2//!
3//! This error type is used to provide context to an error which occurs during the parsing stage.
4use std::{error::Error, fmt::Display};
5
6use super::SpanStack;
7use crate::event::GroupingKind;
8
9/// Anything that could possibly go wrong while parsing.
10///
11/// This error type is used to provide context to an error which occurs during the parsing stage.
12///
13/// The [`Parser`](crate::Parser) implements the [`Iterator`] trait, which returns a stream of `Result<Event, ParserError>`.
14#[derive(Debug)]
15pub struct ParserError {
16    inner: Box<Inner>,
17}
18
19#[derive(Debug)]
20struct Inner {
21    error: ErrorKind,
22    context: Box<str>,
23}
24
25impl ParserError {
26    pub(super) fn new(error: ErrorKind, place: *const u8, span_stack: &mut SpanStack) -> Self {
27        const CONTEXT_SIZE: usize = 12;
28        const CONTEXT_PREFIX: &str = "╭─► context:\n";
29        const EXPANSION_PREFIX: &str = "─► which was expanded from:\n";
30
31        let index = span_stack.reach_original_call_site(place);
32        let mut context = String::from(CONTEXT_PREFIX);
33
34        let first_string = span_stack
35            .expansions
36            .last()
37            .map(|exp| exp.full_expansion)
38            .unwrap_or(span_stack.input);
39
40        let (mut lower_bound, mut upper_bound) = (
41            floor_char_boundary(first_string, index.saturating_sub(CONTEXT_SIZE)),
42            floor_char_boundary(first_string, index + CONTEXT_SIZE),
43        );
44
45        for (index, expansion) in span_stack.expansions.iter().rev().enumerate() {
46            let next_string = (span_stack.expansions.len() - 1)
47                .checked_sub(index + 1)
48                .map(|index| span_stack.expansions[index].full_expansion)
49                .unwrap_or(span_stack.input);
50
51            if lower_bound > expansion.expansion_length {
52                lower_bound += expansion.call_site_in_origin.start;
53                upper_bound =
54                    (expansion.call_site_in_origin.end + upper_bound).min(next_string.len());
55
56                continue;
57            }
58
59            let context_str = &expansion.full_expansion[lower_bound..upper_bound];
60            write_context_str(context_str, &mut context, false, lower_bound > 0);
61            context.push_str(EXPANSION_PREFIX);
62
63            lower_bound = floor_char_boundary(
64                next_string,
65                expansion
66                    .call_site_in_origin
67                    .start
68                    .saturating_sub(CONTEXT_SIZE),
69            );
70            upper_bound = floor_char_boundary(
71                next_string,
72                expansion.call_site_in_origin.end + CONTEXT_SIZE,
73            );
74        }
75        write_context_str(
76            &span_stack.input[lower_bound..upper_bound],
77            &mut context,
78            true,
79            lower_bound > 0,
80        );
81        context.shrink_to_fit();
82
83        Self {
84            inner: Box::new(Inner {
85                error,
86                context: context.into_boxed_str(),
87            }),
88        }
89    }
90}
91
92fn write_context_str(context: &str, out: &mut String, last: bool, has_previous_content: bool) {
93    out.push_str("│\n");
94    let mut lines = context.lines();
95    if let Some(line) = lines.next() {
96        out.push('│');
97        if has_previous_content {
98            out.push('…');
99        } else {
100            out.push(' ');
101        }
102        out.push_str(line);
103        out.push('\n');
104    }
105
106    lines.for_each(|line| {
107        out.push_str("│ ");
108        out.push_str(line);
109        out.push('\n');
110    });
111    let last_line_len = context.lines().last().unwrap_or_default().len();
112    out.push_str("│ ");
113    (0..last_line_len).for_each(|_| out.push('^'));
114    out.push('\n');
115    if last {
116        out.push_str("╰─");
117        (0..last_line_len).for_each(|_| out.push('─'));
118    } else {
119        out.push('├');
120    }
121}
122
123impl Error for ParserError {
124    fn source(&self) -> Option<&(dyn Error + 'static)> {
125        Some(&self.inner.error)
126    }
127}
128
129impl Display for ParserError {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.write_str("parsing error: ")?;
132        self.inner.error.fmt(f)?;
133        f.write_str("\n")?;
134        f.write_str(&self.inner.context)?;
135        Ok(())
136    }
137}
138
139pub(crate) type InnerResult<T> = std::result::Result<T, ErrorKind>;
140
141#[derive(Debug)]
142pub(crate) enum ErrorKind {
143    UnbalancedGroup(Option<GroupingKind>),
144    Environment,
145    MathShift,
146    HashSign,
147    DimensionArgument,
148    DimensionUnit,
149    MathUnit,
150    Delimiter,
151    ControlSequence,
152    Number,
153    CharacterNumber,
154    Argument,
155    GroupArgument,
156    DoubleSubscript,
157    DoubleSuperscript,
158    UnknownPrimitive,
159    ControlSequenceAsArgument,
160    ScriptAsArgument,
161    EmptyControlSequence,
162    UnknownColor,
163    InvalidCharNumber,
164    Relax,
165    BracesInParamText,
166    CommentInParamText,
167    IncorrectMacroParams(u8, u8),
168    IncorrectReplacementParams(u8, u8),
169    TooManyParams,
170    StandaloneHashSign,
171    IncorrectMacroPrefix,
172    MacroSuffixNotFound,
173    MacroAlreadyDefined,
174    MacroNotDefined,
175    Alignment,
176    NewLine,
177    ArrayNoColumns,
178    MissingExpansion,
179    Token,
180}
181
182impl Display for ErrorKind {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        match self {
185            ErrorKind::UnbalancedGroup(Some(missing)) => {
186                write!(f, "unbalanced group found, expected it to be closed with `{}`", missing.closing_str())
187            },
188            ErrorKind::UnbalancedGroup(None) => f.write_str("unbalanced group found, unexpected group closing found"),
189            ErrorKind::Environment => f.write_str("unkown mathematical environment found"),
190            ErrorKind::MathShift => f.write_str(
191                "unexpected math `$` (math shift) character - this character cannot be used inside math mode"),
192            ErrorKind::HashSign => f.write_str(
193                "unexpected hash sign `#` character - this character can only be used in macro definitions"
194            ),
195            ErrorKind::MathUnit => f.write_str("expected mathematical units (mu) in dimension specification"),
196            ErrorKind::Delimiter => f.write_str("expected a delimiter token"),
197            ErrorKind::ControlSequence => f.write_str("expected a control sequence"),
198            ErrorKind::Number => f.write_str("expected a number"),
199            ErrorKind::CharacterNumber => f.write_str("expected a character representing a number after '`'. found a non ascii character"),
200            ErrorKind::Argument => f.write_str("expected an argument"),
201            ErrorKind::GroupArgument => f.write_str("expected an argument delimited by `{{}}`"),
202            ErrorKind::DoubleSubscript => f.write_str("trying to add a subscript twice to the same element"),
203            ErrorKind::DoubleSuperscript => f.write_str("trying to add a superscript twice to the same element"),
204            ErrorKind::UnknownPrimitive => f.write_str("unknown primitive command found"),
205            ErrorKind::ControlSequenceAsArgument => f.write_str("control sequence found as argument to a command that does not support them"),
206            ErrorKind::ScriptAsArgument => f.write_str("subscript and/or superscript found as argument to a command"),
207            ErrorKind::EmptyControlSequence => f.write_str("empty control sequence"),
208            ErrorKind::UnknownColor => f.write_str("unkown color. colors must either be predefined or in the form `#RRGGBB`"),
209            ErrorKind::InvalidCharNumber => f.write_str("expected a number in the range 0..=255 for it to be translated into a character"),
210            ErrorKind::Relax => f.write_str("cannot use the `\\relax` command in this context"),
211            ErrorKind::BracesInParamText => f.write_str("macro definition of parameters contains '{{' or '}}'"),
212            ErrorKind::CommentInParamText => f.write_str("macro definition of parameters contains a (`%`) comment"),
213            ErrorKind::IncorrectMacroParams(found, expected) => {
214                write!(f, "macro definition found parameter #{} but expected #{}", found, expected)
215            }
216            ErrorKind::IncorrectReplacementParams(found, expected) => {
217                write!(f, "macro definition found parameter #{} but expected a parameter in the range [1, {}]", found, expected)
218            }
219            ErrorKind::TooManyParams => f.write_str("macro definition contains too many parameters, the maximum is 9"),
220            ErrorKind::StandaloneHashSign => f.write_str("macro definition contains a standalone '#'"),
221            ErrorKind::IncorrectMacroPrefix => f.write_str("macro use does not match its definition, expected it to begin with a prefix string as specified in the definition"),
222            ErrorKind::MacroSuffixNotFound => f.write_str("macro use does not match its definition, expected its argument(s) to end with a suffix string as specified in the definition"),
223            ErrorKind::MacroAlreadyDefined => f.write_str("macro already defined"),
224            ErrorKind::MacroNotDefined => f.write_str("macro not defined"),
225            ErrorKind::DimensionArgument => f.write_str("expected a dimension or glue argument"),
226            ErrorKind::DimensionUnit => f.write_str("expected a dimensional unit"),
227            ErrorKind::Alignment => f.write_str("alignment not allowed in current environment"),
228            ErrorKind::NewLine => f.write_str("new line command not allowed in current environment"),
229            ErrorKind::ArrayNoColumns => f.write_str("array must have at least one column of the type `c`, `l` or `r`"),
230            ErrorKind::MissingExpansion => f.write_str("The macro definition is missing an expansion"),
231            ErrorKind::Token => f.write_str("expected a token"),
232        }
233    }
234}
235
236impl Error for ErrorKind {}
237
238fn floor_char_boundary(str: &str, index: usize) -> usize {
239    if index >= str.len() {
240        str.len()
241    } else {
242        let lower_bound = index.saturating_sub(3);
243        let new_index = str.as_bytes()[lower_bound..=index].iter().rposition(|b| {
244            // This is bit magic equivalent to: b < 128 || b >= 192
245            (*b as i8) >= -0x40
246        });
247
248        // SAFETY: we know that the character boundary will be within four bytes
249        unsafe { lower_bound + new_index.unwrap_unchecked() }
250    }
251}