Skip to main content

math_core/
error.rs

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