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