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