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