1use alloc::string::String;
2use core::fmt::{self, Write};
3use core::ops::Range;
4
5use kstring::KString;
6use strum_macros::IntoStaticStr;
7
8use crate::environments::Env;
9use crate::html_utils::{escape_double_quoted_html_attribute, escape_html_content};
10use crate::token::EndToken;
11use crate::{MathDisplay, token::LimitsKind};
12
13#[derive(Debug, Clone)]
15pub struct LatexError(pub Range<usize>, pub(crate) LatexErrKind);
16
17#[derive(Debug, Clone)]
18pub(crate) enum LatexErrKind {
19 UnclosedGroup(EndToken),
20 UnmatchedClose(EndToken),
21 ExpectedArgumentGotClose,
22 ExpectedArgumentGotEOI,
23 ExpectedDelimiter(DelimiterModifier),
24 DisallowedChar(char),
25 UnsupportedUnicodeMath(char),
26 UnknownEnvironment(KString),
27 UnknownCommand(KString),
28 UnknownColor(KString),
29 MismatchedEnvironment {
30 expected: Env,
31 got: Env,
32 },
33 CannotBeUsedHere {
34 got: LimitedUsabilityToken,
35 correct_place: Place,
36 },
37 ExpectedRelation,
38 ExpectedLargeOp,
39 ExpectedAtMostOneToken,
40 ExpectedExactlyOneToken,
41 BoundFollowedByBound,
42 DuplicateSubOrSup,
43 CannotBeUsedAsArgument,
44 ExpectedAscii,
45 ExpectedLength(KString),
46 IllegalUnit {
47 unit: KString,
48 math_unit_expected: bool,
49 },
50 InvalidUnit(KString),
51 ExpectedColSpec(KString),
52 ExpectedStyle,
53 NotValidInTextMode,
54 NotValidInMathMode,
55 NestedMathModeUnimplemented,
57 UnexpectedDollar,
59 CouldNotExtractText,
60 MoreThanOneLabel,
61 MoreThanOneInfixCmd,
62 InvalidMacroName(String),
63 ExpectedCommandName,
65 CommandAlreadyDefined,
67 CommandNotDefined,
69 InvalidParameterNumber,
70 ParameterNumberOutOfRange {
71 n: u8,
72 actual: u8,
73 },
74 DelimitedParameters,
76 UnexpectedParameterNumber {
78 expected: u8,
79 actual: u8,
80 },
81 MacroParameterOutsideCustomCommand,
82 ExpectedParamNumberGotEOI,
83 HardLimitExceeded,
84 TooManyExpansions,
85 Internal,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
89pub enum DelimiterModifier {
90 #[strum(serialize = r"\left")]
91 Left,
92 #[strum(serialize = r"\right")]
93 Right,
94 #[strum(serialize = r"\middle")]
95 Middle,
96 #[strum(serialize = r"\big, \Big, ...")]
97 Big,
98 #[strum(serialize = r"\genfrac")]
99 Genfrac,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
103#[repr(u32)] pub enum Place {
105 #[strum(serialize = r"after \int, \sum, ...")]
106 AfterBigOp,
107 #[strum(serialize = r"in a table-like environment")]
108 TableEnv,
109 #[strum(serialize = r"at the top level of a table-like environment")]
110 TableEnvTopLevel,
111 #[strum(serialize = r"in a numbered equation environment")]
112 NumberedEnv,
113 #[strum(serialize = r"directly after a `\\` or at the beginning of an array or matrix")]
114 ArrayRowStart,
115 #[strum(serialize = r"directly after a `\\` or at the beginning of a multline environment")]
116 MultlineRowStart,
117 #[strum(serialize = r"directly before \let or \def")]
118 BeforeDefinition,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
122pub enum LimitedUsabilityToken {
123 #[strum(serialize = "&")]
124 Ampersand,
125 #[strum(serialize = r"\\")]
126 NewLine,
127 #[strum(serialize = r"\tag[*]")]
128 Tag,
129 #[strum(serialize = r"\label")]
130 Label,
131 #[strum(serialize = r"\limits")]
132 Limits,
133 #[strum(serialize = r"\nolimits")]
134 NoLimits,
135 #[strum(serialize = r"\displaylimits")]
136 DisplayLimits,
137 #[strum(serialize = r"\h[dash]line")]
138 HLine,
139 #[strum(serialize = r"\global")]
140 Global,
141 #[strum(serialize = r"\shove(left|right)")]
142 Shove,
143}
144
145impl From<LimitsKind> for LimitedUsabilityToken {
146 fn from(kind: LimitsKind) -> Self {
147 match kind {
148 LimitsKind::Always => LimitedUsabilityToken::Limits,
149 LimitsKind::Never => LimitedUsabilityToken::NoLimits,
150 LimitsKind::Display => LimitedUsabilityToken::DisplayLimits,
151 }
152 }
153}
154
155impl LatexErrKind {
156 fn write_msg(&self, s: &mut String) -> core::fmt::Result {
158 match self {
159 LatexErrKind::UnclosedGroup(expected) => {
160 write!(
161 s,
162 "Expected closing token \"{}\", but reached end of input.",
163 <&str>::from(expected)
164 )?;
165 }
166 LatexErrKind::UnmatchedClose(got) => {
167 write!(s, "Unmatched closing token: \"{}\".", <&str>::from(got))?;
168 }
169 LatexErrKind::ExpectedArgumentGotClose => {
170 write!(
171 s,
172 r"Expected argument but got closing token (`}}`, `\end`, `\right`)."
173 )?;
174 }
175 LatexErrKind::ExpectedArgumentGotEOI => {
176 write!(s, "Expected argument but reached end of input.")?;
177 }
178 LatexErrKind::ExpectedDelimiter(location) => {
179 write!(
180 s,
181 "There must be a parenthesis after \"{}\", but not found.",
182 <&str>::from(*location)
183 )?;
184 }
185 LatexErrKind::ExpectedStyle => {
186 write!(
187 s,
188 r"Expected one of `\displaystyle`, `\textstyle`, `\scriptstyle`, or `\scriptscriptstyle`"
189 )?;
190 }
191 LatexErrKind::DisallowedChar(got) => {
192 write!(s, "Disallowed character in text group: '{got}'.")?;
193 }
194 LatexErrKind::UnsupportedUnicodeMath(got) => {
195 write!(
196 s,
197 "Direct Unicode input is not supported for this symbol yet: '{got}'."
198 )?;
199 }
200 LatexErrKind::UnknownEnvironment(environment) => {
201 write!(s, "Unknown environment \"{environment}\".")?;
202 }
203 LatexErrKind::UnknownCommand(cmd) => {
204 write!(s, "Unknown command \"\\{cmd}\".")?;
205 }
206 LatexErrKind::UnknownColor(color) => {
207 write!(s, "Unknown color \"{color}\".")?;
208 }
209 LatexErrKind::MismatchedEnvironment { expected, got } => {
210 write!(
211 s,
212 "Expected \"\\end{{{}}}\", but found \"\\end{{{}}}\".",
213 expected.as_str(),
214 got.as_str()
215 )?;
216 }
217 LatexErrKind::CannotBeUsedHere { got, correct_place } => {
218 write!(
219 s,
220 "Found \"{}\", which may only appear {}.",
221 <&str>::from(got),
222 <&str>::from(correct_place)
223 )?;
224 }
225 LatexErrKind::ExpectedRelation => {
226 write!(s, "Expected a relation after \\not.")?;
227 }
228 LatexErrKind::ExpectedLargeOp => {
229 write!(s, "Expected a large operator.")?;
230 }
231 LatexErrKind::ExpectedAtMostOneToken => {
232 write!(s, "Expected at most one token as argument.")?;
233 }
234 LatexErrKind::ExpectedExactlyOneToken => {
235 write!(s, "Expected exactly one token as argument.")?;
236 }
237 LatexErrKind::BoundFollowedByBound => {
238 write!(s, "'^' or '_' directly followed by '^', '_' or prime.")?;
239 }
240 LatexErrKind::DuplicateSubOrSup => {
241 write!(s, "Duplicate subscript or superscript.")?;
242 }
243 LatexErrKind::CannotBeUsedAsArgument => {
244 write!(s, "Switch-like commands cannot be used as arguments.")?;
245 }
246 LatexErrKind::ExpectedAscii => {
247 write!(
248 s,
249 "Expected non-special ASCII characters in string literal."
250 )?;
251 }
252 LatexErrKind::ExpectedLength(got) => {
253 write!(s, "Expected length with units, found \"{got}\".")?;
254 }
255 LatexErrKind::IllegalUnit {
256 unit,
257 math_unit_expected,
258 } => {
259 if *math_unit_expected {
260 write!(
261 s,
262 "Text unit \"{unit}\" cannot be used with \\mkern/\\mskip/\\mspace."
263 )?;
264 } else {
265 write!(
266 s,
267 "Math unit \"{unit}\" cannot be used with \\kern/\\hskip/\\hspace."
268 )?;
269 }
270 }
271 LatexErrKind::InvalidUnit(unit) => {
272 write!(s, "Found invalid unit \"{unit}\".")?;
273 }
274 LatexErrKind::ExpectedColSpec(got) => {
275 write!(s, "Expected column specification, found \"{got}\".")?;
276 }
277 LatexErrKind::NotValidInTextMode => {
278 write!(s, "Not valid in text mode.")?;
279 }
280 LatexErrKind::NotValidInMathMode => {
281 write!(s, "Not valid in math mode.")?;
282 }
283 LatexErrKind::NestedMathModeUnimplemented => {
284 write!(s, "Math mode within text mode is not implemented yet.")?;
285 }
286 LatexErrKind::UnexpectedDollar => {
287 write!(s, "Unexpected \"$\".")?;
288 }
289 LatexErrKind::CouldNotExtractText => {
290 write!(s, "Could not extract text from the given macro.")?;
291 }
292 LatexErrKind::MoreThanOneLabel => {
293 write!(s, "Found more than one label in a row.")?;
294 }
295 LatexErrKind::MoreThanOneInfixCmd => {
296 write!(s, "Found more than one infix fraction in a group.")?;
297 }
298 LatexErrKind::InvalidMacroName(name) => {
299 write!(s, "Invalid macro name: \"\\{name}\".")?;
300 }
301 LatexErrKind::ExpectedCommandName => {
302 write!(s, "Expected the name of a command.")?;
303 }
304 LatexErrKind::CommandAlreadyDefined => {
305 write!(s, "This command is already defined.")?;
306 }
307 LatexErrKind::CommandNotDefined => {
308 write!(s, "This command is not defined.")?;
309 }
310 LatexErrKind::InvalidParameterNumber => {
311 write!(s, "Invalid parameter number. Must be 1-9.")?;
312 }
313 LatexErrKind::ParameterNumberOutOfRange { n, actual } => {
314 write!(
315 s,
316 "Parameter number {actual} is out of range. Expected a number of at most {n}."
317 )?;
318 }
319 LatexErrKind::DelimitedParameters => {
320 write!(
321 s,
322 "Delimited parameters are not supported. Expected \"#n\" or \"{{\" here."
323 )?;
324 }
325 LatexErrKind::UnexpectedParameterNumber { expected, actual } => {
326 write!(
327 s,
328 "Expected parameter #{expected}, found #{actual}. Parameters must be numbered consecutively, starting at 1."
329 )?;
330 }
331 LatexErrKind::MacroParameterOutsideCustomCommand => {
332 write!(
333 s,
334 "Macro parameter found outside of custom command definition."
335 )?;
336 }
337 LatexErrKind::ExpectedParamNumberGotEOI => {
338 write!(
339 s,
340 "Expected parameter number after '#', but reached end of input."
341 )?;
342 }
343 LatexErrKind::HardLimitExceeded => {
344 write!(s, "Hard limit exceeded. Please simplify your equation.")?;
345 }
346 LatexErrKind::TooManyExpansions => {
347 write!(
348 s,
349 "Too many expansions of custom commands. A command may be expanding to itself."
350 )?;
351 }
352 LatexErrKind::Internal => {
353 write!(
354 s,
355 "Internal parser error. Please report this bug at https://github.com/tmke8/math-core/issues"
356 )?;
357 }
358 }
359 Ok(())
360 }
361}
362
363impl LatexError {
364 pub fn to_html(&self, latex: &str, display: MathDisplay, css_class: Option<&str>) -> String {
372 let mut output = String::new();
373 let tag = if matches!(display, MathDisplay::Block) {
374 "p"
375 } else {
376 "span"
377 };
378 let css_class = css_class.unwrap_or("math-core-error");
379 let _ = write!(output, r#"<{tag} class="{css_class}" title=""#);
380 let mut err_msg = String::new();
381 self.to_message(&mut err_msg, latex);
382 escape_double_quoted_html_attribute(&mut output, &err_msg);
383 output.push_str(r#""><code>"#);
384 escape_html_content(&mut output, latex);
385 let _ = write!(output, "</code></{tag}>");
386 output
387 }
388
389 pub fn error_message(&self) -> String {
391 let mut s = String::new();
392 let _ = self.1.write_msg(&mut s);
393 s
394 }
395
396 pub fn to_message(&self, s: &mut String, input: &str) {
403 let loc = input.floor_char_boundary(self.0.start);
404 let codepoint_offset = input[..loc].chars().count();
405 let _ = write!(s, "{codepoint_offset}: ");
406 let _ = self.1.write_msg(s);
407 }
408
409 pub fn label(&self) -> &'static str {
411 match &self.1 {
412 LatexErrKind::UnclosedGroup(_) => "a group was never closed",
413 LatexErrKind::UnmatchedClose(_) => "no matching opening for this",
414 LatexErrKind::ExpectedArgumentGotClose | LatexErrKind::ExpectedArgumentGotEOI => {
415 "expected an argument here"
416 }
417 LatexErrKind::ExpectedDelimiter(_) => "expected a delimiter here",
418 LatexErrKind::DisallowedChar(_) => "disallowed character",
419 LatexErrKind::UnsupportedUnicodeMath(_) => "unsupported math symbol",
420 LatexErrKind::UnknownEnvironment(_) => "unknown environment",
421 LatexErrKind::UnknownCommand(_) => "unknown command",
422 LatexErrKind::UnknownColor(_) => "unknown color",
423 LatexErrKind::MismatchedEnvironment { .. } => {
424 "expected a different environment name here"
425 }
426 LatexErrKind::CannotBeUsedHere { .. } => "cannot be used here",
427 LatexErrKind::ExpectedRelation => "expected a relation",
428 LatexErrKind::ExpectedLargeOp => "expected a large operator",
429 LatexErrKind::ExpectedStyle => "expected a style",
430 LatexErrKind::ExpectedAtMostOneToken => "expected at most one token here",
431 LatexErrKind::ExpectedExactlyOneToken => "expected exactly one token here",
432 LatexErrKind::BoundFollowedByBound => "unexpected bound",
433 LatexErrKind::DuplicateSubOrSup => "duplicate",
434 LatexErrKind::CannotBeUsedAsArgument => "used as argument",
435 LatexErrKind::ExpectedAscii => "special or not ASCII",
436 LatexErrKind::ExpectedLength(_) => "expected length here",
437 LatexErrKind::IllegalUnit { .. } => "illegal unit here",
438 LatexErrKind::InvalidUnit(_) => "invalid unit here",
439 LatexErrKind::ExpectedColSpec(_) => "expected a column spec here",
440 LatexErrKind::NotValidInTextMode => "this is not valid in text mode",
441 LatexErrKind::NotValidInMathMode => "this is not valid in math mode",
442 LatexErrKind::NestedMathModeUnimplemented => "cannot switch to math mode here",
443 LatexErrKind::UnexpectedDollar => "unexpected dollar sign",
444 LatexErrKind::CouldNotExtractText => "could not extract text from this",
445 LatexErrKind::MoreThanOneLabel => "duplicate label",
446 LatexErrKind::MoreThanOneInfixCmd => "duplicate infix frac",
447 LatexErrKind::InvalidMacroName(_) => "invalid name here",
448 LatexErrKind::ExpectedCommandName => "expected a command name here",
449 LatexErrKind::CommandAlreadyDefined => "already defined",
450 LatexErrKind::CommandNotDefined => "not defined",
451 LatexErrKind::InvalidParameterNumber => "must be 1-9",
452 LatexErrKind::ParameterNumberOutOfRange { .. } => "parameter number out of range",
453 LatexErrKind::DelimitedParameters => "unsupported delimiter",
454 LatexErrKind::UnexpectedParameterNumber { .. } => "unexpected parameter number",
455 LatexErrKind::MacroParameterOutsideCustomCommand => "unexpected macro parameter",
456 LatexErrKind::ExpectedParamNumberGotEOI => "expected parameter number",
457 LatexErrKind::HardLimitExceeded => "limit exceeded",
458 LatexErrKind::TooManyExpansions => "expansion limit exceeded",
459 LatexErrKind::Internal => "internal error",
460 }
461 }
462}
463
464#[cfg(feature = "ariadne")]
465impl LatexError {
466 pub fn to_report<'name>(
468 &self,
469 source_name: &'name str,
470 with_color: bool,
471 ) -> ariadne::Report<'static, (&'name str, Range<usize>)> {
472 use ariadne::{Label, Report, ReportKind};
473
474 let label_msg = self.label();
475
476 let mut config = ariadne::Config::default().with_index_type(ariadne::IndexType::Byte);
477 if !with_color {
478 config = config.with_color(false);
479 }
480 Report::build(ReportKind::Error, (source_name, self.0.start..self.0.start))
481 .with_config(config)
482 .with_message(self.error_message())
483 .with_label(Label::new((source_name, self.0.clone())).with_message(label_msg))
484 .finish()
485 }
486}
487
488impl fmt::Display for LatexError {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 write!(f, "{}", self.error_message())
491 }
492}
493
494impl core::error::Error for LatexError {}
495
496pub trait GetUnwrap {
497 fn get_unwrap(&self, range: core::ops::Range<usize>) -> &str;
499}
500
501impl GetUnwrap for str {
502 #[cfg(target_arch = "wasm32")]
503 #[inline]
504 fn get_unwrap(&self, range: core::ops::Range<usize>) -> &str {
505 unsafe { self.get_unchecked(range) }
508 }
509 #[cfg(not(target_arch = "wasm32"))]
510 #[inline]
511 fn get_unwrap(&self, range: core::ops::Range<usize>) -> &str {
512 self.get(range).expect("valid range")
513 }
514}