Skip to main content

math_core/
error.rs

1use std::fmt::{self, Write};
2use std::ops::Range;
3
4use strum_macros::IntoStaticStr;
5
6// use no_panic::no_panic;
7
8use crate::MathDisplay;
9use crate::environments::Env;
10use crate::html_utils::{escape_double_quoted_html_attribute, escape_html_content};
11use crate::token::EndToken;
12
13/// Represents an error that occurred during LaTeX parsing or rendering.
14#[derive(Debug, Clone)]
15pub struct LatexError(pub Range<usize>, pub LatexErrKind);
16
17#[derive(Debug, Clone)]
18#[non_exhaustive]
19pub enum LatexErrKind {
20    UnclosedGroup(EndToken),
21    UnmatchedClose(EndToken),
22    ExpectedArgumentGotClose,
23    ExpectedArgumentGotEOF,
24    ExpectedDelimiter(DelimiterModifier),
25    DisallowedChar(char),
26    UnknownEnvironment(Box<str>),
27    UnknownCommand(Box<str>),
28    UnknownColor(Box<str>),
29    MismatchedEnvironment {
30        expected: Env,
31        got: Env,
32    },
33    CannotBeUsedHere {
34        got: LimitedUsabilityToken,
35        correct_place: Place,
36    },
37    ExpectedRelation,
38    BoundFollowedByBound,
39    DuplicateSubOrSup,
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    ExpectedParamNumberGotEOF,
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}
64
65#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
66#[repr(u32)] // A different value here somehow increases code size on WASM enormously.
67pub enum Place {
68    #[strum(serialize = r"after \int, \sum, ...")]
69    AfterBigOp,
70    #[strum(serialize = r"in a table-like environment")]
71    TableEnv,
72    #[strum(serialize = r"in a numbered equation environment")]
73    NumberedEnv,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
77pub enum LimitedUsabilityToken {
78    #[strum(serialize = "&")]
79    Ampersand,
80    #[strum(serialize = r"\tag")]
81    Tag,
82    #[strum(serialize = r"\limits")]
83    Limits,
84}
85
86impl LatexErrKind {
87    /// Returns the error message as a string.
88    ///
89    /// This serves the same purpose as the `Display` implementation,
90    /// but produces more compact WASM code.
91    pub fn string(&self) -> String {
92        match self {
93            LatexErrKind::UnclosedGroup(expected) => {
94                "Expected token \"".to_string() + <&str>::from(expected) + "\", but not found."
95            }
96            LatexErrKind::UnmatchedClose(got) => {
97                "Unmatched closing token: \"".to_string() + <&str>::from(got) + "\"."
98            }
99            LatexErrKind::ExpectedArgumentGotClose => {
100                r"Expected argument but got closing token (`}`, `\end`, `\right`).".to_string()
101            }
102            LatexErrKind::ExpectedArgumentGotEOF => "Expected argument but reached end of input.".to_string(),
103            LatexErrKind::ExpectedDelimiter(location) => {
104                "There must be a parenthesis after \"".to_string()
105                    + <&str>::from(*location)
106                    + "\", but not found."
107            }
108            LatexErrKind::DisallowedChar(got) => {
109                let mut text = "Disallowed character in text group: '".to_string();
110                text.push(*got);
111                text += "'.";
112                text
113            }
114            LatexErrKind::UnknownEnvironment(environment) => {
115                "Unknown environment \"".to_string() + environment + "\"."
116            }
117            LatexErrKind::UnknownCommand(cmd) => "Unknown command \"\\".to_string() + cmd + "\".",
118            LatexErrKind::UnknownColor(color) => "Unknown color \"".to_string() + color + "\".",
119            LatexErrKind::MismatchedEnvironment { expected, got } => {
120                "Expected \"\\end{".to_string()
121                    + expected.as_str()
122                    + "}\", but got \"\\end{"
123                    + got.as_str()
124                    + "}\"."
125            }
126            LatexErrKind::CannotBeUsedHere { got, correct_place } => {
127                "Got \"".to_string()
128                    + <&str>::from(got)
129                    + "\", which may only appear "
130                    + <&str>::from(correct_place)
131                    + "."
132            }
133            LatexErrKind::ExpectedRelation => {
134                "Expected a relation after \\not.".to_string()
135            }
136            LatexErrKind::BoundFollowedByBound => {
137                "'^' or '_' directly followed by '^', '_' or prime.".to_string()
138            }
139            LatexErrKind::DuplicateSubOrSup => {
140                "Duplicate subscript or superscript.".to_string()
141            }
142            LatexErrKind::ExpectedText(place) => "Expected text in ".to_string() + place + ".",
143            LatexErrKind::ExpectedLength(got) => {
144                "Expected length with units, got \"".to_string() + got + "\"."
145            }
146            LatexErrKind::ExpectedNumber(got) => "Expected a number, got \"".to_string() + got + "\".",
147            LatexErrKind::ExpectedColSpec(got) => {
148                "Expected column specification, got \"".to_string() + got + "\"."
149            }
150            LatexErrKind::NotValidInTextMode => {
151                "Not valid in text mode.".to_string()
152            }
153            LatexErrKind::InvalidMacroName(name) => {
154                "Invalid macro name: \"\\".to_string() + name + "\"."
155            }
156            LatexErrKind::InvalidParameterNumber => {
157                "Invalid parameter number. Must be 1-9.".to_string()
158            }
159            LatexErrKind::MacroParameterOutsideCustomCommand => {
160                "Macro parameter found outside of custom command definition.".to_string()
161            }
162            LatexErrKind::ExpectedParamNumberGotEOF => {
163                "Expected parameter number after '#', but got end of input.".to_string()
164            }
165            LatexErrKind::HardLimitExceeded => {
166                "Hard limit exceeded. Please simplify your equation.".to_string()
167            }
168            LatexErrKind::Internal => {
169                "Internal parser error. Please report this bug at https://github.com/tmke8/math-core/issues".to_string()
170            },
171        }
172    }
173}
174
175impl LatexError {
176    /// Format a LaTeX error as an HTML snippet.
177    ///
178    /// # Arguments
179    /// - `latex`: The original LaTeX input that caused the error.
180    /// - `display`: The display mode of the equation (inline or block).
181    /// - `css_class`: An optional CSS class to apply to the error element. If `None`,
182    ///   defaults to `"math-core-error"`.
183    pub fn to_html(&self, latex: &str, display: MathDisplay, css_class: Option<&str>) -> String {
184        let mut output = String::new();
185        let tag = if matches!(display, MathDisplay::Block) {
186            "p"
187        } else {
188            "span"
189        };
190        let css_class = css_class.unwrap_or("math-core-error");
191        let _ = write!(
192            output,
193            r#"<{} class="{}" title="{}: "#,
194            tag, css_class, self.0.start
195        );
196        escape_double_quoted_html_attribute(&mut output, &self.1.string());
197        output.push_str(r#""><code>"#);
198        escape_html_content(&mut output, latex);
199        let _ = write!(output, "</code></{tag}>");
200        output
201    }
202}
203
204impl fmt::Display for LatexError {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "{}: {}", self.0.start, self.1.string())
207    }
208}
209
210impl std::error::Error for LatexError {}
211
212pub trait GetUnwrap {
213    /// `str::get` with `Option::unwrap`.
214    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str;
215}
216
217impl GetUnwrap for str {
218    #[cfg(target_arch = "wasm32")]
219    #[inline]
220    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
221        // On WASM, panics are really expensive in terms of code size,
222        // so we use an unchecked get here.
223        unsafe { self.get_unchecked(range) }
224    }
225    #[cfg(not(target_arch = "wasm32"))]
226    #[inline]
227    fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
228        self.get(range).expect("valid range")
229    }
230}
231
232/* fn itoa(val: u64, buf: &mut [u8; 20]) -> &str {
233    let start = if val == 0 {
234        buf[19] = b'0';
235        19
236    } else {
237        let mut val = val;
238        let mut i = 20;
239
240        // The `i > 0` check is technically redundant but it allows the compiler to
241        // prove that `buf` is always validly indexed.
242        while val != 0 && i > 0 {
243            i -= 1;
244            buf[i] = (val % 10) as u8 + b'0';
245            val /= 10;
246        }
247        i
248    };
249
250    // This is safe because we know the buffer contains valid ASCII.
251    // This unsafe block wouldn't be necessary if the `ascii_char` feature were stable.
252    unsafe { std::str::from_utf8_unchecked(&buf[start..]) }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn itoa_test() {
261        let mut buf = [0u8; 20];
262        assert_eq!(itoa(0, &mut buf), "0");
263        assert_eq!(itoa(1, &mut buf), "1");
264        assert_eq!(itoa(10, &mut buf), "10");
265        assert_eq!(itoa(1234567890, &mut buf), "1234567890");
266        assert_eq!(itoa(u64::MAX, &mut buf), "18446744073709551615");
267    }
268} */