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