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