use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use crate::detect::{Detected, DiagramType};
use crate::js::chars::{is_word, len16, trim_end};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ErrorCode {
UnsupportedDiagram,
Syntax,
Semantic,
Limit,
}
impl ErrorCode {
pub fn as_str(self) -> &'static str {
match self {
ErrorCode::UnsupportedDiagram => "unsupported-diagram",
ErrorCode::Syntax => "syntax",
ErrorCode::Semantic => "semantic",
ErrorCode::Limit => "limit",
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Error {
pub code: ErrorCode,
pub message: String,
pub diagram: Option<DiagramType>,
pub mermaid_type: Option<&'static str>,
pub line: usize,
pub column: usize,
pub snippet: String,
}
impl Error {
pub(crate) fn new(message: impl Into<String>, code: ErrorCode) -> Error {
Error {
code,
message: message.into(),
diagram: None,
mermaid_type: None,
line: 0,
column: 0,
snippet: String::new(),
}
}
pub(crate) fn of(mut self, found: Detected) -> Error {
self.diagram = Some(found.diagram);
self.mermaid_type = Some(found.mermaid_type);
self
}
pub(crate) fn tagged(mut self, found: Detected) -> Error {
self.diagram.get_or_insert(found.diagram);
self.mermaid_type.get_or_insert(found.mermaid_type);
self.message = written_codes(&self.message);
self.snippet = written_codes(&self.snippet);
self
}
}
impl Error {
pub fn byte_offset(&self, src: &str) -> Option<usize> {
if self.line == 0 || self.column == 0 {
return None;
}
let mut start = 0;
for _ in 1..self.line {
start += src[start..].find('\n')? + 1;
}
let line = &src[start..];
let line = &line[..line.find('\n').unwrap_or(line.len())];
let mut units = self.column - 1;
for (i, c) in line.char_indices() {
if units < c.len_utf16() {
return (units == 0).then_some(start + i);
}
units -= c.len_utf16();
}
(units == 0).then_some(start + line.len())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug)]
pub(crate) struct Edit {
pub(crate) from: usize,
pub(crate) to: usize,
pub(crate) text: String,
}
impl Edit {
pub(crate) fn cut(from: usize, to: usize) -> Edit {
Edit {
from,
to,
text: String::new(),
}
}
}
pub(crate) type Steps<'a> = Option<&'a mut Vec<Vec<Edit>>>;
pub(crate) fn push(steps: &mut Steps, edits: Vec<Edit>) {
if let Some(steps) = steps {
steps.push(edits);
}
}
fn step_input(edits: &[Edit], at: usize) -> usize {
let mut shift: isize = 0;
for e in edits {
let start = (e.from as isize - shift) as usize;
if at < start {
break;
}
if at < start + e.text.len() {
return e.from + len16(&e.text[..at - start]).min(e.to - e.from - 1);
}
shift += e.to as isize - e.from as isize - e.text.len() as isize;
}
(at as isize + shift) as usize
}
pub(crate) fn input_offset(steps: &[Vec<Edit>], at: usize) -> usize {
steps
.iter()
.rev()
.fold(at, |at, edits| step_input(edits, at))
}
pub(crate) fn written_codes(text: &str) -> String {
const OPEN: &str = "fl°";
const CLOSE: &str = "¶ß";
if !text.contains(CLOSE) {
return text.to_string();
}
let mut out = String::new();
let (mut last, mut i) = (0, 0);
while let Some(n) = text[i..].find(OPEN) {
let at = i + n;
let mut j = at + OPEN.len();
if text[j..].starts_with('°') {
j += '°'.len_utf8();
}
let k = j + text[j..]
.bytes()
.take_while(|&c| is_word(i32::from(c)))
.count();
if k > j && text[k..].starts_with(CLOSE) {
out.push_str(&text[last..at]);
out.push('#');
out.push_str(&text[j..k]);
out.push(';');
last = k + CLOSE.len();
i = last;
continue;
}
i = at + OPEN.len();
}
out.push_str(&text[last..]);
out
}
pub(crate) struct Origin {
pub(crate) parsed: Rc<str>,
pub(crate) find: Box<dyn Fn(usize) -> (String, usize)>,
}
thread_local! {
static ORIGIN: RefCell<Option<Rc<Origin>>> = const { RefCell::new(None) };
}
pub(crate) fn locating_in<T>(next: Origin, run: impl FnOnce() -> T) -> T {
struct Restore(Option<Rc<Origin>>);
impl Drop for Restore {
fn drop(&mut self) {
let outer = self.0.take();
ORIGIN.with(|o| *o.borrow_mut() = outer);
}
}
let outer = ORIGIN.with(|o| o.borrow_mut().replace(Rc::new(next)));
let _restore = Restore(outer);
run()
}
pub(crate) fn written(src: &str, offset: usize) -> (String, usize) {
match ORIGIN.with(|o| o.borrow().clone()) {
Some(origin) if *origin.parsed == *src => (origin.find)(offset),
_ => (src.to_string(), offset),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Location {
pub(crate) line: usize,
pub(crate) column: usize,
pub(crate) snippet: String,
}
pub(crate) fn locate(src: &str, offset: usize) -> Location {
let end = trim_end(src).len();
let (src, mut offset) = if end > 0 && offset >= end {
let last = src[..end].chars().next_back().map_or(1, char::len_utf8);
let (src, at) = written(src, end - last);
let size = src[at.min(src.len())..]
.chars()
.next()
.map_or(0, char::len_utf8);
(src, at + size)
} else {
written(src, offset)
};
offset = offset.min(src.len());
while !src.is_char_boundary(offset) {
offset -= 1;
}
let line_start = src[..offset].rfind('\n').map_or(0, |i| i + 1);
let line = 1 + src[..offset].matches('\n').count();
let line_end = src[offset..].find('\n').map_or(src.len(), |i| offset + i);
let column = len16(&src[line_start..offset]) + 1;
let snippet = format!(
"{}\n{}^",
&src[line_start..line_end],
"-".repeat(column - 1)
);
Location {
line,
column,
snippet,
}
}
pub(crate) fn syntax_error(
diagram: DiagramType,
src: &str,
offset: usize,
detail: &str,
lexical: bool,
) -> Error {
let loc = locate(src, offset);
let message = if lexical {
format!(
"Lexical error on line {}. Unrecognized text.\n{}",
loc.line, loc.snippet
)
} else {
format!(
"Parse error on line {}:\n{}\n{detail}",
loc.line, loc.snippet
)
};
Error {
code: ErrorCode::Syntax,
message,
diagram: Some(diagram),
mermaid_type: None,
line: loc.line,
column: loc.column,
snippet: loc.snippet,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_the_byte_of_a_place() {
let src = "graph TD\n a😀b --> ä¸\n\nx\n";
for (at, _) in src.char_indices().chain([(src.len(), ' ')]) {
let loc = locate_in(src, at);
let e = Error {
line: loc.line,
column: loc.column,
..Error::new("", ErrorCode::Syntax)
};
assert_eq!(e.byte_offset(src), Some(at), "{at}");
}
let at = |line, column| {
Error {
line,
column,
..Error::new("", ErrorCode::Syntax)
}
.byte_offset(src)
};
assert_eq!(at(2, 6), Some(16));
assert_eq!(at(2, 5), None);
assert_eq!(at(1, 9), Some(8));
assert_eq!(at(1, 10), None);
assert_eq!(at(6, 1), None);
assert_eq!(at(0, 0), None);
}
fn locate_in(src: &str, offset: usize) -> Location {
let line_start = src[..offset].rfind('\n').map_or(0, |i| i + 1);
Location {
line: 1 + src[..offset].matches('\n').count(),
column: len16(&src[line_start..offset]) + 1,
snippet: String::new(),
}
}
}