Skip to main content

math_core/
error.rs

1use std::fmt::{self, Write};
2use std::ops::Range;
3
4use strum_macros::IntoStaticStr;
5
6use crate::environments::Env;
7use crate::html_utils::{escape_double_quoted_html_attribute, escape_html_content};
8use crate::token::EndToken;
9use crate::{MathDisplay, token::LimitsKind};
10
11/// Represents an error that occurred during LaTeX parsing.
12#[derive(Debug, Clone)]
13pub struct LatexError(pub Range<usize>, pub(crate) LatexErrKind);
14
15#[derive(Debug, Clone)]
16pub(crate) enum LatexErrKind {
17    UnclosedGroup(EndToken),
18    UnmatchedClose(EndToken),
19    ExpectedArgumentGotClose,
20    ExpectedArgumentGotEOI,
21    ExpectedDelimiter(DelimiterModifier),
22    DisallowedChar(char),
23    UnknownEnvironment(Box<str>),
24    UnknownCommand(Box<str>),
25    UnknownColor(Box<str>),
26    MismatchedEnvironment {
27        expected: Env,
28        got: Env,
29    },
30    CannotBeUsedHere {
31        got: LimitedUsabilityToken,
32        correct_place: Place,
33    },
34    ExpectedRelation,
35    ExpectedLargeOp,
36    ExpectedAtMostOneToken,
37    ExpectedExactlyOneToken,
38    BoundFollowedByBound,
39    DuplicateSubOrSup,
40    CannotBeUsedAsArgument,
41    ExpectedAscii,
42    ExpectedLength(Box<str>),
43    IllegalUnit {
44        unit: Box<str>,
45        math_unit_expected: bool,
46    },
47    InvalidUnit(Box<str>),
48    ExpectedColSpec(Box<str>),
49    ExpectedStyle,
50    NotValidInTextMode,
51    NotValidInMathMode,
52    CouldNotExtractText,
53    MoreThanOneLabel,
54    MoreThanOneInfixCmd,
55    InvalidMacroName(String),
56    InvalidParameterNumber,
57    MacroParameterOutsideCustomCommand,
58    ExpectedParamNumberGotEOI,
59    HardLimitExceeded,
60    Internal,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
64pub enum DelimiterModifier {
65    #[strum(serialize = r"\left")]
66    Left,
67    #[strum(serialize = r"\right")]
68    Right,
69    #[strum(serialize = r"\middle")]
70    Middle,
71    #[strum(serialize = r"\big, \Big, ...")]
72    Big,
73    #[strum(serialize = r"\genfrac")]
74    Genfrac,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
78#[repr(u32)] // A different value here somehow increases code size on WASM enormously.
79pub enum Place {
80    #[strum(serialize = r"after \int, \sum, ...")]
81    AfterBigOp,
82    #[strum(serialize = r"in a table-like environment")]
83    TableEnv,
84    #[strum(serialize = r"in a numbered equation environment")]
85    NumberedEnv,
86    #[strum(serialize = r"directly after a `\\` or at the beginning of an array")]
87    ArrayRowStart,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
91pub enum LimitedUsabilityToken {
92    #[strum(serialize = "&")]
93    Ampersand,
94    #[strum(serialize = r"\tag")]
95    Tag,
96    #[strum(serialize = r"\label")]
97    Label,
98    #[strum(serialize = r"\limits")]
99    Limits,
100    #[strum(serialize = r"\nolimits")]
101    NoLimits,
102    #[strum(serialize = r"\displaylimits")]
103    DisplayLimits,
104    #[strum(serialize = r"\hline")]
105    HLine,
106    #[strum(serialize = r"\hdashline")]
107    HDashLine,
108}
109
110impl From<LimitsKind> for LimitedUsabilityToken {
111    fn from(kind: LimitsKind) -> Self {
112        match kind {
113            LimitsKind::Always => LimitedUsabilityToken::Limits,
114            LimitsKind::Never => LimitedUsabilityToken::NoLimits,
115            LimitsKind::Display => LimitedUsabilityToken::DisplayLimits,
116        }
117    }
118}
119
120impl LatexErrKind {
121    /// Returns the error message as a string.
122    fn write_msg(&self, s: &mut String) -> std::fmt::Result {
123        match self {
124            LatexErrKind::UnclosedGroup(expected) => {
125                write!(
126                    s,
127                    "Expected closing token \"{}\", but reached end of input.",
128                    <&str>::from(expected)
129                )?;
130            }
131            LatexErrKind::UnmatchedClose(got) => {
132                write!(s, "Unmatched closing token: \"{}\".", <&str>::from(got))?;
133            }
134            LatexErrKind::ExpectedArgumentGotClose => {
135                write!(
136                    s,
137                    r"Expected argument but got closing token (`}}`, `\end`, `\right`)."
138                )?;
139            }
140            LatexErrKind::ExpectedArgumentGotEOI => {
141                write!(s, "Expected argument but reached end of input.")?;
142            }
143            LatexErrKind::ExpectedDelimiter(location) => {
144                write!(
145                    s,
146                    "There must be a parenthesis after \"{}\", but not found.",
147                    <&str>::from(*location)
148                )?;
149            }
150            LatexErrKind::ExpectedStyle => {
151                write!(
152                    s,
153                    r"Expected one of `\displaystyle`, `\textstyle`, `\scriptstyle`, or `\scriptscriptstyle`"
154                )?;
155            }
156            LatexErrKind::DisallowedChar(got) => {
157                write!(s, "Disallowed character in text group: '{got}'.")?;
158            }
159            LatexErrKind::UnknownEnvironment(environment) => {
160                write!(s, "Unknown environment \"{environment}\".")?;
161            }
162            LatexErrKind::UnknownCommand(cmd) => {
163                write!(s, "Unknown command \"\\{cmd}\".")?;
164            }
165            LatexErrKind::UnknownColor(color) => {
166                write!(s, "Unknown color \"{color}\".")?;
167            }
168            LatexErrKind::MismatchedEnvironment { expected, got } => {
169                write!(
170                    s,
171                    "Expected \"\\end{{{}}}\", but found \"\\end{{{}}}\".",
172                    expected.as_str(),
173                    got.as_str()
174                )?;
175            }
176            LatexErrKind::CannotBeUsedHere { got, correct_place } => {
177                write!(
178                    s,
179                    "Found \"{}\", which may only appear {}.",
180                    <&str>::from(got),
181                    <&str>::from(correct_place)
182                )?;
183            }
184            LatexErrKind::ExpectedRelation => {
185                write!(s, "Expected a relation after \\not.")?;
186            }
187            LatexErrKind::ExpectedLargeOp => {
188                write!(s, "Expected a large operator.")?;
189            }
190            LatexErrKind::ExpectedAtMostOneToken => {
191                write!(s, "Expected at most one token as argument.")?;
192            }
193            LatexErrKind::ExpectedExactlyOneToken => {
194                write!(s, "Expected exactly one token as argument.")?;
195            }
196            LatexErrKind::BoundFollowedByBound => {
197                write!(s, "'^' or '_' directly followed by '^', '_' or prime.")?;
198            }
199            LatexErrKind::DuplicateSubOrSup => {
200                write!(s, "Duplicate subscript or superscript.")?;
201            }
202            LatexErrKind::CannotBeUsedAsArgument => {
203                write!(s, "Switch-like commands cannot be used as arguments.")?;
204            }
205            LatexErrKind::ExpectedAscii => {
206                write!(
207                    s,
208                    "Expected non-special ASCII characters in string literal."
209                )?;
210            }
211            LatexErrKind::ExpectedLength(got) => {
212                write!(s, "Expected length with units, found \"{got}\".")?;
213            }
214            LatexErrKind::IllegalUnit {
215                unit,
216                math_unit_expected,
217            } => {
218                if *math_unit_expected {
219                    write!(
220                        s,
221                        "Text unit \"{unit}\" cannot be used with \\mkern/\\mskip/\\mspace."
222                    )?;
223                } else {
224                    write!(
225                        s,
226                        "Math unit \"{unit}\" cannot be used with \\kern/\\hskip/\\hspace."
227                    )?;
228                }
229            }
230            LatexErrKind::InvalidUnit(unit) => {
231                write!(s, "Found invalid unit \"{unit}\".")?;
232            }
233            LatexErrKind::ExpectedColSpec(got) => {
234                write!(s, "Expected column specification, found \"{got}\".")?;
235            }
236            LatexErrKind::NotValidInTextMode => {
237                write!(s, "Not valid in text mode.")?;
238            }
239            LatexErrKind::NotValidInMathMode => {
240                write!(s, "Not valid in math mode.")?;
241            }
242            LatexErrKind::CouldNotExtractText => {
243                write!(s, "Could not extract text from the given macro.")?;
244            }
245            LatexErrKind::MoreThanOneLabel => {
246                write!(s, "Found more than one label in a row.")?;
247            }
248            LatexErrKind::MoreThanOneInfixCmd => {
249                write!(s, "Found more than one infix fraction in a group.")?;
250            }
251            LatexErrKind::InvalidMacroName(name) => {
252                write!(s, "Invalid macro name: \"\\{name}\".")?;
253            }
254            LatexErrKind::InvalidParameterNumber => {
255                write!(s, "Invalid parameter number. Must be 1-9.")?;
256            }
257            LatexErrKind::MacroParameterOutsideCustomCommand => {
258                write!(
259                    s,
260                    "Macro parameter found outside of custom command definition."
261                )?;
262            }
263            LatexErrKind::ExpectedParamNumberGotEOI => {
264                write!(
265                    s,
266                    "Expected parameter number after '#', but reached end of input."
267                )?;
268            }
269            LatexErrKind::HardLimitExceeded => {
270                write!(s, "Hard limit exceeded. Please simplify your equation.")?;
271            }
272            LatexErrKind::Internal => {
273                write!(
274                    s,
275                    "Internal parser error. Please report this bug at https://github.com/tmke8/math-core/issues"
276                )?;
277            }
278        }
279        Ok(())
280    }
281}
282
283impl LatexError {
284    /// Format a LaTeX error as an HTML snippet.
285    ///
286    /// # Arguments
287    /// - `latex`: The original LaTeX input that caused the error.
288    /// - `display`: The display mode of the equation (inline or block).
289    /// - `css_class`: An optional CSS class to apply to the error element. If `None`,
290    ///   defaults to `"math-core-error"`.
291    pub fn to_html(&self, latex: &str, display: MathDisplay, css_class: Option<&str>) -> String {
292        let mut output = String::new();
293        let tag = if matches!(display, MathDisplay::Block) {
294            "p"
295        } else {
296            "span"
297        };
298        let css_class = css_class.unwrap_or("math-core-error");
299        let _ = write!(output, r#"<{tag} class="{css_class}" title=""#);
300        let mut err_msg = String::new();
301        self.to_message(&mut err_msg, latex);
302        escape_double_quoted_html_attribute(&mut output, &err_msg);
303        output.push_str(r#""><code>"#);
304        escape_html_content(&mut output, latex);
305        let _ = write!(output, "</code></{tag}>");
306        output
307    }
308
309    /// Returns only the error message itself as a string.
310    pub fn error_message(&self) -> String {
311        let mut s = String::new();
312        let _ = self.1.write_msg(&mut s);
313        s
314    }
315
316    /// Format a LaTeX error as a plain text message, including the source name and position.
317    ///
318    /// # Arguments
319    /// - `s`: The string to write the message into.
320    /// - `input`: The original LaTeX input that caused the error; used to
321    ///   calculate the character offset for the error position.
322    pub fn to_message(&self, s: &mut String, input: &str) {
323        let loc = input.floor_char_boundary(self.0.start);
324        let codepoint_offset = input[..loc].chars().count();
325        let _ = write!(s, "{codepoint_offset}: ");
326        let _ = self.1.write_msg(s);
327    }
328
329    /// Returns a short label for the main error location.
330    pub fn label(&self) -> &'static str {
331        match &self.1 {
332            LatexErrKind::UnclosedGroup(_) => "a group was never closed",
333            LatexErrKind::UnmatchedClose(_) => "no matching opening for this",
334            LatexErrKind::ExpectedArgumentGotClose | LatexErrKind::ExpectedArgumentGotEOI => {
335                "expected an argument here"
336            }
337            LatexErrKind::ExpectedDelimiter(_) => "expected a delimiter here",
338            LatexErrKind::DisallowedChar(_) => "disallowed character",
339            LatexErrKind::UnknownEnvironment(_) => "unknown environment",
340            LatexErrKind::UnknownCommand(_) => "unknown command",
341            LatexErrKind::UnknownColor(_) => "unknown color",
342            LatexErrKind::MismatchedEnvironment { .. } => {
343                "expected a different environment name here"
344            }
345            LatexErrKind::CannotBeUsedHere { .. } => "cannot be used here",
346            LatexErrKind::ExpectedRelation => "expected a relation",
347            LatexErrKind::ExpectedLargeOp => "expected a large operator",
348            LatexErrKind::ExpectedStyle => "expected a style",
349            LatexErrKind::ExpectedAtMostOneToken => "expected at most one token here",
350            LatexErrKind::ExpectedExactlyOneToken => "expected exactly one token here",
351            LatexErrKind::BoundFollowedByBound => "unexpected bound",
352            LatexErrKind::DuplicateSubOrSup => "duplicate",
353            LatexErrKind::CannotBeUsedAsArgument => "used as argument",
354            LatexErrKind::ExpectedAscii => "special or not ASCII",
355            LatexErrKind::ExpectedLength(_) => "expected length here",
356            LatexErrKind::IllegalUnit { .. } => "illegal unit here",
357            LatexErrKind::InvalidUnit(_) => "invalid unit here",
358            LatexErrKind::ExpectedColSpec(_) => "expected a column spec here",
359            LatexErrKind::NotValidInTextMode => "this is not valid in text mode",
360            LatexErrKind::NotValidInMathMode => "this is not valid in math mode",
361            LatexErrKind::CouldNotExtractText => "could not extract text from this",
362            LatexErrKind::MoreThanOneLabel => "duplicate label",
363            LatexErrKind::MoreThanOneInfixCmd => "duplicate infix frac",
364            LatexErrKind::InvalidMacroName(_) => "invalid name here",
365            LatexErrKind::InvalidParameterNumber => "must be 1-9",
366            LatexErrKind::MacroParameterOutsideCustomCommand => "unexpected macro parameter",
367            LatexErrKind::ExpectedParamNumberGotEOI => "expected parameter number",
368            LatexErrKind::HardLimitExceeded => "limit exceeded",
369            LatexErrKind::Internal => "internal error",
370        }
371    }
372}
373
374#[cfg(feature = "ariadne")]
375impl LatexError {
376    /// Convert this error into an [`ariadne::Report`] for pretty-printing.
377    pub fn to_report<'name>(
378        &self,
379        source_name: &'name str,
380        with_color: bool,
381    ) -> ariadne::Report<'static, (&'name str, Range<usize>)> {
382        use ariadne::{Label, Report, ReportKind};
383
384        let label_msg = self.label();
385
386        let mut config = ariadne::Config::default().with_index_type(ariadne::IndexType::Byte);
387        if !with_color {
388            config = config.with_color(false);
389        }
390        Report::build(ReportKind::Error, (source_name, self.0.start..self.0.start))
391            .with_config(config)
392            .with_message(self.error_message())
393            .with_label(Label::new((source_name, self.0.clone())).with_message(label_msg))
394            .finish()
395    }
396}
397
398impl fmt::Display for LatexError {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        write!(f, "{}", self.error_message())
401    }
402}
403
404impl std::error::Error for LatexError {}
405
406pub trait GetUnwrap {
407    /// `str::get` with `Option::unwrap`.
408    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str;
409}
410
411impl GetUnwrap for str {
412    #[cfg(target_arch = "wasm32")]
413    #[inline]
414    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
415        // On WASM, panics are really expensive in terms of code size,
416        // so we use an unchecked get here.
417        unsafe { self.get_unchecked(range) }
418    }
419    #[cfg(not(target_arch = "wasm32"))]
420    #[inline]
421    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
422        self.get(range).expect("valid range")
423    }
424}