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