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