Skip to main content

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