math_core/
error.rs

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