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