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