1use std::borrow::Cow;
2use std::fmt::{self, Write};
3use std::ops::Range;
4
5use strum_macros::IntoStaticStr;
6
7use crate::environments::Env;
8use crate::html_utils::{escape_double_quoted_html_attribute, escape_html_content};
9use crate::token::EndToken;
10use crate::{MathDisplay, token::LimitsKind};
11
12#[derive(Debug, Clone)]
14pub struct LatexError(pub Range<usize>, pub(crate) LatexErrKind);
15
16#[derive(Debug, Clone)]
17pub(crate) enum LatexErrKind {
18 UnclosedGroup(EndToken),
19 UnmatchedClose(EndToken),
20 ExpectedArgumentGotClose,
21 ExpectedArgumentGotEOI,
22 ExpectedDelimiter(DelimiterModifier),
23 DisallowedChar(char),
24 UnknownEnvironment(Box<str>),
25 UnknownCommand(Box<str>),
26 UnknownColor(Box<str>),
27 MismatchedEnvironment {
28 expected: Env,
29 got: Env,
30 },
31 CannotBeUsedHere {
32 got: LimitedUsabilityToken,
33 correct_place: Place,
34 },
35 ExpectedRelation,
36 ExpectedLargeOp,
37 UnsupportedMathClassArgument,
38 ExpectedAtMostOneToken,
39 ExpectedExactlyOneToken,
40 BoundFollowedByBound,
41 DuplicateSubOrSup,
42 CannotBeUsedAsArgument,
43 ExpectedText,
44 ExpectedLength(Box<str>),
45 IllegalUnit {
46 unit: Box<str>,
47 math_unit_expected: bool,
48 },
49 InvalidUnit(Box<str>),
50 NegativeLengthNotSupported,
51 ExpectedColSpec(Box<str>),
52 ExpectedNumber(Box<str>),
53 ExpectedStyle,
54 NotValidInTextMode,
55 NotValidInMathMode,
56 CouldNotExtractText,
57 MoreThanOneLabel,
58 MoreThanOneInfixCmd,
59 UndefinedLabel(Box<str>),
60 InvalidMacroName(String),
61 InvalidParameterNumber,
62 MacroParameterOutsideCustomCommand,
63 ExpectedParamNumberGotEOI,
64 HardLimitExceeded,
65 Internal,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
69pub enum DelimiterModifier {
70 #[strum(serialize = r"\left")]
71 Left,
72 #[strum(serialize = r"\right")]
73 Right,
74 #[strum(serialize = r"\middle")]
75 Middle,
76 #[strum(serialize = r"\big, \Big, ...")]
77 Big,
78 #[strum(serialize = r"\genfrac")]
79 Genfrac,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
83#[repr(u32)] pub enum Place {
85 #[strum(serialize = r"after \int, \sum, ...")]
86 AfterBigOp,
87 #[strum(serialize = r"in a table-like environment")]
88 TableEnv,
89 #[strum(serialize = r"in a numbered equation environment")]
90 NumberedEnv,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
94pub enum LimitedUsabilityToken {
95 #[strum(serialize = "&")]
96 Ampersand,
97 #[strum(serialize = r"\tag")]
98 Tag,
99 #[strum(serialize = r"\label")]
100 Label,
101 #[strum(serialize = r"\limits")]
102 Limits,
103 #[strum(serialize = r"\nolimits")]
104 NoLimits,
105 #[strum(serialize = r"\displaylimits")]
106 DisplayLimits,
107}
108
109impl From<LimitsKind> for LimitedUsabilityToken {
110 fn from(kind: LimitsKind) -> Self {
111 match kind {
112 LimitsKind::Always => LimitedUsabilityToken::Limits,
113 LimitsKind::Never => LimitedUsabilityToken::NoLimits,
114 LimitsKind::Display => LimitedUsabilityToken::DisplayLimits,
115 }
116 }
117}
118
119impl LatexErrKind {
120 fn write_msg(&self, s: &mut String) -> std::fmt::Result {
122 match self {
123 LatexErrKind::UnclosedGroup(expected) => {
124 write!(
125 s,
126 "Expected token \"{}\", but not found.",
127 <&str>::from(expected)
128 )?;
129 }
130 LatexErrKind::UnmatchedClose(got) => {
131 write!(s, "Unmatched closing token: \"{}\".", <&str>::from(got))?;
132 }
133 LatexErrKind::ExpectedArgumentGotClose => {
134 write!(
135 s,
136 r"Expected argument but got closing token (`}}`, `\end`, `\right`)."
137 )?;
138 }
139 LatexErrKind::ExpectedArgumentGotEOI => {
140 write!(s, "Expected argument but reached end of input.")?;
141 }
142 LatexErrKind::ExpectedDelimiter(location) => {
143 write!(
144 s,
145 "There must be a parenthesis after \"{}\", but not found.",
146 <&str>::from(*location)
147 )?;
148 }
149 LatexErrKind::ExpectedStyle => {
150 write!(
151 s,
152 r"Expected one of `\displaystyle`, `\textstyle`, `\scriptstyle`, or `\scriptscriptstyle`"
153 )?;
154 }
155 LatexErrKind::DisallowedChar(got) => {
156 write!(s, "Disallowed character in text group: '{got}'.")?;
157 }
158 LatexErrKind::UnknownEnvironment(environment) => {
159 write!(s, "Unknown environment \"{environment}\".")?;
160 }
161 LatexErrKind::UnknownCommand(cmd) => {
162 write!(s, "Unknown command \"\\{cmd}\".")?;
163 }
164 LatexErrKind::UnknownColor(color) => {
165 write!(s, "Unknown color \"{color}\".")?;
166 }
167 LatexErrKind::MismatchedEnvironment { expected, got } => {
168 write!(
169 s,
170 "Expected \"\\end{{{}}}\", but got \"\\end{{{}}}\".",
171 expected.as_str(),
172 got.as_str()
173 )?;
174 }
175 LatexErrKind::CannotBeUsedHere { got, correct_place } => {
176 write!(
177 s,
178 "Got \"{}\", which may only appear {}.",
179 <&str>::from(got),
180 <&str>::from(correct_place)
181 )?;
182 }
183 LatexErrKind::ExpectedRelation => {
184 write!(s, "Expected a relation after \\not.")?;
185 }
186 LatexErrKind::ExpectedLargeOp => {
187 write!(s, "Expected a large operator.")?;
188 }
189 LatexErrKind::UnsupportedMathClassArgument => {
190 write!(
191 s,
192 "Support for this argument isn't implemented yet for class-changing commands."
193 )?;
194 }
195 LatexErrKind::ExpectedAtMostOneToken => {
196 write!(s, "Expected at most one token as argument.")?;
197 }
198 LatexErrKind::ExpectedExactlyOneToken => {
199 write!(s, "Expected exactly one token as argument.")?;
200 }
201 LatexErrKind::BoundFollowedByBound => {
202 write!(s, "'^' or '_' directly followed by '^', '_' or prime.")?;
203 }
204 LatexErrKind::DuplicateSubOrSup => {
205 write!(s, "Duplicate subscript or superscript.")?;
206 }
207 LatexErrKind::CannotBeUsedAsArgument => {
208 write!(s, "Switch-like commands cannot be used as arguments.")?;
209 }
210 LatexErrKind::ExpectedText => {
211 write!(s, "Expected text in string literal.")?;
212 }
213 LatexErrKind::ExpectedLength(got) => {
214 write!(s, "Expected length with units, got \"{got}\".")?;
215 }
216 LatexErrKind::IllegalUnit {
217 unit,
218 math_unit_expected,
219 } => {
220 if *math_unit_expected {
221 write!(
222 s,
223 "Text unit \"{unit}\" cannot be used with \\mkern/\\mskip/\\mspace."
224 )?;
225 } else {
226 write!(
227 s,
228 "Math unit \"{unit}\" cannot be used with \\kern/\\hskip/\\hspace."
229 )?;
230 }
231 }
232 LatexErrKind::InvalidUnit(unit) => {
233 write!(s, "Found invalid unit \"{unit}\".")?;
234 }
235 LatexErrKind::NegativeLengthNotSupported => {
236 write!(s, "Negative values are currently not supported.")?;
237 }
238 LatexErrKind::ExpectedNumber(got) => {
239 write!(s, "Expected a number, got \"{got}\".")?;
240 }
241 LatexErrKind::ExpectedColSpec(got) => {
242 write!(s, "Expected column specification, got \"{got}\".")?;
243 }
244 LatexErrKind::NotValidInTextMode => {
245 write!(s, "Not valid in text mode.")?;
246 }
247 LatexErrKind::NotValidInMathMode => {
248 write!(s, "Not valid in math mode.")?;
249 }
250 LatexErrKind::CouldNotExtractText => {
251 write!(s, "Could not extract text from the given macro.")?;
252 }
253 LatexErrKind::MoreThanOneLabel => {
254 write!(s, "Found more than one label in a row.")?;
255 }
256 LatexErrKind::MoreThanOneInfixCmd => {
257 write!(s, "Found more than one infix fraction in a group.")?;
258 }
259 LatexErrKind::UndefinedLabel(label) => {
260 write!(s, "Found undefined label: \"{label}\".")?;
261 }
262 LatexErrKind::InvalidMacroName(name) => {
263 write!(s, "Invalid macro name: \"\\{name}\".")?;
264 }
265 LatexErrKind::InvalidParameterNumber => {
266 write!(s, "Invalid parameter number. Must be 1-9.")?;
267 }
268 LatexErrKind::MacroParameterOutsideCustomCommand => {
269 write!(
270 s,
271 "Macro parameter found outside of custom command definition."
272 )?;
273 }
274 LatexErrKind::ExpectedParamNumberGotEOI => {
275 write!(
276 s,
277 "Expected parameter number after '#', but got end of input."
278 )?;
279 }
280 LatexErrKind::HardLimitExceeded => {
281 write!(s, "Hard limit exceeded. Please simplify your equation.")?;
282 }
283 LatexErrKind::Internal => {
284 write!(
285 s,
286 "Internal parser error. Please report this bug at https://github.com/tmke8/math-core/issues"
287 )?;
288 }
289 }
290 Ok(())
291 }
292}
293
294impl LatexError {
295 pub fn to_html(&self, latex: &str, display: MathDisplay, css_class: Option<&str>) -> String {
303 let mut output = String::new();
304 let tag = if matches!(display, MathDisplay::Block) {
305 "p"
306 } else {
307 "span"
308 };
309 let css_class = css_class.unwrap_or("math-core-error");
310 let _ = write!(output, r#"<{tag} class="{css_class}" title=""#);
311 let mut err_msg = String::new();
312 self.to_message(&mut err_msg, latex);
313 escape_double_quoted_html_attribute(&mut output, &err_msg);
314 output.push_str(r#""><code>"#);
315 escape_html_content(&mut output, latex);
316 let _ = write!(output, "</code></{tag}>");
317 output
318 }
319
320 pub fn error_message(&self) -> String {
322 let mut s = String::new();
323 let _ = self.1.write_msg(&mut s);
324 s
325 }
326
327 pub fn to_message(&self, s: &mut String, input: &str) {
334 let loc = input.floor_char_boundary(self.0.start);
335 let codepoint_offset = input[..loc].chars().count();
336 let _ = write!(s, "{codepoint_offset}: ");
337 let _ = self.1.write_msg(s);
338 }
339
340 pub fn label(&self) -> Cow<'static, str> {
342 match &self.1 {
343 LatexErrKind::UnclosedGroup(expected) => format!(
344 "expected \"{}\" to close this group",
345 <&str>::from(expected)
346 )
347 .into(),
348 LatexErrKind::UnmatchedClose(_) => "no matching opening for this".into(),
349 LatexErrKind::ExpectedArgumentGotClose | LatexErrKind::ExpectedArgumentGotEOI => {
350 "expected an argument here".into()
351 }
352 LatexErrKind::ExpectedDelimiter(_) => "expected a delimiter here".into(),
353 LatexErrKind::DisallowedChar(_) => "disallowed character".into(),
354 LatexErrKind::UnknownEnvironment(_) => "unknown environment".into(),
355 LatexErrKind::UnknownCommand(_) => "unknown command".into(),
356 LatexErrKind::UnknownColor(_) => "unknown color".into(),
357 LatexErrKind::MismatchedEnvironment { expected, .. } => {
358 format!("expected \"\\end{{{}}}\" here", expected.as_str()).into()
359 }
360 LatexErrKind::CannotBeUsedHere { correct_place, .. } => {
361 format!("may only appear {}", <&str>::from(correct_place)).into()
362 }
363 LatexErrKind::ExpectedRelation => "expected a relation".into(),
364 LatexErrKind::ExpectedLargeOp => "expected a large operator".into(),
365 LatexErrKind::ExpectedStyle => "expected a style".into(),
366 LatexErrKind::UnsupportedMathClassArgument => "unsupported argument".into(),
367 LatexErrKind::ExpectedAtMostOneToken => "expected at most one token here".into(),
368 LatexErrKind::ExpectedExactlyOneToken => "expected exactly one token here".into(),
369 LatexErrKind::BoundFollowedByBound => "unexpected bound".into(),
370 LatexErrKind::DuplicateSubOrSup => "duplicate".into(),
371 LatexErrKind::CannotBeUsedAsArgument => "used as argument".into(),
372 LatexErrKind::ExpectedText => "expected text here".into(),
373 LatexErrKind::ExpectedLength(_) => "expected length here".into(),
374 LatexErrKind::IllegalUnit { .. } => "illegal unit here".into(),
375 LatexErrKind::InvalidUnit(_) => "invalid unit here".into(),
376 LatexErrKind::NegativeLengthNotSupported => "negative value here".into(),
377 LatexErrKind::ExpectedNumber(_) => "expected a number here".into(),
378 LatexErrKind::ExpectedColSpec(_) => "expected a column spec here".into(),
379 LatexErrKind::NotValidInTextMode => "this is not valid in text mode".into(),
380 LatexErrKind::NotValidInMathMode => "this is not valid in math mode".into(),
381 LatexErrKind::CouldNotExtractText => "could not extract text from this".into(),
382 LatexErrKind::MoreThanOneLabel => "duplicate label".into(),
383 LatexErrKind::MoreThanOneInfixCmd => "duplicate infix frac".into(),
384 LatexErrKind::UndefinedLabel(_) => "label has not been previously defined".into(),
385 LatexErrKind::InvalidMacroName(_) => "invalid name here".into(),
386 LatexErrKind::InvalidParameterNumber => "must be 1-9".into(),
387 LatexErrKind::MacroParameterOutsideCustomCommand => "unexpected macro parameter".into(),
388 LatexErrKind::ExpectedParamNumberGotEOI => "expected parameter number".into(),
389 LatexErrKind::HardLimitExceeded => "limit exceeded".into(),
390 LatexErrKind::Internal => "internal error".into(),
391 }
392 }
393}
394
395#[cfg(feature = "ariadne")]
396impl LatexError {
397 pub fn to_report<'name>(
399 &self,
400 source_name: &'name str,
401 with_color: bool,
402 ) -> ariadne::Report<'static, (&'name str, Range<usize>)> {
403 use ariadne::{Label, Report, ReportKind};
404
405 let label_msg = self.label();
406
407 let mut config = ariadne::Config::default().with_index_type(ariadne::IndexType::Byte);
408 if !with_color {
409 config = config.with_color(false);
410 }
411 Report::build(ReportKind::Error, (source_name, self.0.start..self.0.start))
412 .with_config(config)
413 .with_message(self.error_message())
414 .with_label(Label::new((source_name, self.0.clone())).with_message(label_msg))
415 .finish()
416 }
417}
418
419impl fmt::Display for LatexError {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write!(f, "{}", self.error_message())
422 }
423}
424
425impl std::error::Error for LatexError {}
426
427pub trait GetUnwrap {
428 fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str;
430}
431
432impl GetUnwrap for str {
433 #[cfg(target_arch = "wasm32")]
434 #[inline]
435 fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
436 unsafe { self.get_unchecked(range) }
439 }
440 #[cfg(not(target_arch = "wasm32"))]
441 #[inline]
442 fn get_unwrap(&self, range: std::ops::Range<usize>) -> &str {
443 self.get(range).expect("valid range")
444 }
445}