1use std::fmt::{self, Write};
2
3use strum_macros::IntoStaticStr;
4
5use 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#[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)] pub 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 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 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 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 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