1use std::cell::RefCell;
5use std::fmt;
6use std::rc::Rc;
7
8use crate::detect::{Detected, DiagramType};
9use crate::js::chars::{is_word, len16, trim_end};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum ErrorCode {
14 UnsupportedDiagram,
16 Syntax,
18 Semantic,
20 Limit,
22}
23
24impl ErrorCode {
25 pub fn as_str(self) -> &'static str {
27 match self {
28 ErrorCode::UnsupportedDiagram => "unsupported-diagram",
29 ErrorCode::Syntax => "syntax",
30 ErrorCode::Semantic => "semantic",
31 ErrorCode::Limit => "limit",
32 }
33 }
34}
35
36#[derive(Clone, Debug, PartialEq)]
38#[non_exhaustive]
39pub struct Error {
40 pub code: ErrorCode,
42 pub message: String,
44 pub diagram: Option<DiagramType>,
46 pub mermaid_type: Option<&'static str>,
48 pub line: usize,
50 pub column: usize,
53 pub snippet: String,
55}
56
57impl Error {
58 pub(crate) fn new(message: impl Into<String>, code: ErrorCode) -> Error {
60 Error {
61 code,
62 message: message.into(),
63 diagram: None,
64 mermaid_type: None,
65 line: 0,
66 column: 0,
67 snippet: String::new(),
68 }
69 }
70
71 pub(crate) fn of(mut self, found: Detected) -> Error {
73 self.diagram = Some(found.diagram);
74 self.mermaid_type = Some(found.mermaid_type);
75 self
76 }
77
78 pub(crate) fn tagged(mut self, found: Detected) -> Error {
82 self.diagram.get_or_insert(found.diagram);
83 self.mermaid_type.get_or_insert(found.mermaid_type);
84 self.message = written_codes(&self.message);
85 self.snippet = written_codes(&self.snippet);
86 self
87 }
88}
89
90impl Error {
91 pub fn byte_offset(&self, src: &str) -> Option<usize> {
96 if self.line == 0 || self.column == 0 {
97 return None;
98 }
99 let mut start = 0;
100 for _ in 1..self.line {
101 start += src[start..].find('\n')? + 1;
102 }
103 let line = &src[start..];
104 let line = &line[..line.find('\n').unwrap_or(line.len())];
105 let mut units = self.column - 1;
106 for (i, c) in line.char_indices() {
107 if units < c.len_utf16() {
108 return (units == 0).then_some(start + i);
109 }
110 units -= c.len_utf16();
111 }
112 (units == 0).then_some(start + line.len())
113 }
114}
115
116impl fmt::Display for Error {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(&self.message)
119 }
120}
121
122impl std::error::Error for Error {}
123
124#[derive(Clone, Debug)]
128pub(crate) struct Edit {
129 pub(crate) from: usize,
130 pub(crate) to: usize,
131 pub(crate) text: String,
132}
133
134impl Edit {
135 pub(crate) fn cut(from: usize, to: usize) -> Edit {
137 Edit {
138 from,
139 to,
140 text: String::new(),
141 }
142 }
143}
144
145pub(crate) type Steps<'a> = Option<&'a mut Vec<Vec<Edit>>>;
148
149pub(crate) fn push(steps: &mut Steps, edits: Vec<Edit>) {
153 if let Some(steps) = steps {
154 steps.push(edits);
155 }
156}
157
158fn step_input(edits: &[Edit], at: usize) -> usize {
162 let mut shift: isize = 0;
163 for e in edits {
164 let start = (e.from as isize - shift) as usize;
165 if at < start {
166 break;
167 }
168 if at < start + e.text.len() {
169 return e.from + len16(&e.text[..at - start]).min(e.to - e.from - 1);
170 }
171 shift += e.to as isize - e.from as isize - e.text.len() as isize;
172 }
173 (at as isize + shift) as usize
174}
175
176pub(crate) fn input_offset(steps: &[Vec<Edit>], at: usize) -> usize {
179 steps
180 .iter()
181 .rev()
182 .fold(at, |at, edits| step_input(edits, at))
183}
184
185pub(crate) fn written_codes(text: &str) -> String {
189 const OPEN: &str = "fl°";
190 const CLOSE: &str = "¶ß";
191 if !text.contains(CLOSE) {
192 return text.to_string();
193 }
194 let mut out = String::new();
195 let (mut last, mut i) = (0, 0);
196 while let Some(n) = text[i..].find(OPEN) {
197 let at = i + n;
198 let mut j = at + OPEN.len();
199 if text[j..].starts_with('°') {
200 j += '°'.len_utf8();
201 }
202 let k = j + text[j..]
203 .bytes()
204 .take_while(|&c| is_word(i32::from(c)))
205 .count();
206 if k > j && text[k..].starts_with(CLOSE) {
207 out.push_str(&text[last..at]);
208 out.push('#');
209 out.push_str(&text[j..k]);
210 out.push(';');
211 last = k + CLOSE.len();
212 i = last;
213 continue;
214 }
215 i = at + OPEN.len();
218 }
219 out.push_str(&text[last..]);
220 out
221}
222
223pub(crate) struct Origin {
226 pub(crate) parsed: Rc<str>,
227 pub(crate) find: Box<dyn Fn(usize) -> (String, usize)>,
228}
229
230thread_local! {
231 static ORIGIN: RefCell<Option<Rc<Origin>>> = const { RefCell::new(None) };
234}
235
236pub(crate) fn locating_in<T>(next: Origin, run: impl FnOnce() -> T) -> T {
239 struct Restore(Option<Rc<Origin>>);
241 impl Drop for Restore {
242 fn drop(&mut self) {
243 let outer = self.0.take();
244 ORIGIN.with(|o| *o.borrow_mut() = outer);
245 }
246 }
247 let outer = ORIGIN.with(|o| o.borrow_mut().replace(Rc::new(next)));
248 let _restore = Restore(outer);
249 run()
250}
251
252pub(crate) fn written(src: &str, offset: usize) -> (String, usize) {
256 match ORIGIN.with(|o| o.borrow().clone()) {
257 Some(origin) if *origin.parsed == *src => (origin.find)(offset),
258 _ => (src.to_string(), offset),
259 }
260}
261
262#[derive(Clone, Debug, PartialEq, Eq)]
265pub(crate) struct Location {
266 pub(crate) line: usize,
267 pub(crate) column: usize,
268 pub(crate) snippet: String,
269}
270
271pub(crate) fn locate(src: &str, offset: usize) -> Location {
275 let end = trim_end(src).len();
277 let (src, mut offset) = if end > 0 && offset >= end {
278 let last = src[..end].chars().next_back().map_or(1, char::len_utf8);
281 let (src, at) = written(src, end - last);
282 let size = src[at.min(src.len())..]
283 .chars()
284 .next()
285 .map_or(0, char::len_utf8);
286 (src, at + size)
287 } else {
288 written(src, offset)
289 };
290 offset = offset.min(src.len());
291 while !src.is_char_boundary(offset) {
292 offset -= 1;
293 }
294 let line_start = src[..offset].rfind('\n').map_or(0, |i| i + 1);
295 let line = 1 + src[..offset].matches('\n').count();
296 let line_end = src[offset..].find('\n').map_or(src.len(), |i| offset + i);
297 let column = len16(&src[line_start..offset]) + 1;
298 let snippet = format!(
299 "{}\n{}^",
300 &src[line_start..line_end],
301 "-".repeat(column - 1)
302 );
303 Location {
304 line,
305 column,
306 snippet,
307 }
308}
309
310pub(crate) fn syntax_error(
313 diagram: DiagramType,
314 src: &str,
315 offset: usize,
316 detail: &str,
317 lexical: bool,
318) -> Error {
319 let loc = locate(src, offset);
320 let message = if lexical {
321 format!(
322 "Lexical error on line {}. Unrecognized text.\n{}",
323 loc.line, loc.snippet
324 )
325 } else {
326 format!(
327 "Parse error on line {}:\n{}\n{detail}",
328 loc.line, loc.snippet
329 )
330 };
331 Error {
332 code: ErrorCode::Syntax,
333 message,
334 diagram: Some(diagram),
335 mermaid_type: None,
336 line: loc.line,
337 column: loc.column,
338 snippet: loc.snippet,
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn finds_the_byte_of_a_place() {
348 let src = "graph TD\n a😀b --> 中\n\nx\n";
349 for (at, _) in src.char_indices().chain([(src.len(), ' ')]) {
350 let loc = locate_in(src, at);
351 let e = Error {
352 line: loc.line,
353 column: loc.column,
354 ..Error::new("", ErrorCode::Syntax)
355 };
356 assert_eq!(e.byte_offset(src), Some(at), "{at}");
357 }
358 let at = |line, column| {
359 Error {
360 line,
361 column,
362 ..Error::new("", ErrorCode::Syntax)
363 }
364 .byte_offset(src)
365 };
366 assert_eq!(at(2, 6), Some(16));
369 assert_eq!(at(2, 5), None);
370 assert_eq!(at(1, 9), Some(8));
371 assert_eq!(at(1, 10), None);
372 assert_eq!(at(6, 1), None);
373 assert_eq!(at(0, 0), None);
374 }
375
376 fn locate_in(src: &str, offset: usize) -> Location {
379 let line_start = src[..offset].rfind('\n').map_or(0, |i| i + 1);
380 Location {
381 line: 1 + src[..offset].matches('\n').count(),
382 column: len16(&src[line_start..offset]) + 1,
383 snippet: String::new(),
384 }
385 }
386}