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