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