Skip to main content

math_core/
error.rs

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