use super::db::SankeyDb;
use super::lexer::{
T_COMMA, T_DQUOTE, T_END, T_EOF, T_ESCAPED_TEXT, T_NEWLINE, T_NON_ESCAPED_TEXT, T_SANKEY,
T_STUCK, TOKEN_NAMES, tokenize,
};
use crate::detect::DiagramType;
use crate::errors::{Edit, Error, Steps, input_offset, push, syntax_error};
use crate::js::chars::{is_space, trim, trim_start, unit_at, unit_before};
use crate::js::number::parse_float;
use crate::text::decode_entities;
fn is_blank(c: i32) -> bool {
c != 10 && c != 13 && is_space(c)
}
fn trim_rows(text: &str, edits: &mut Vec<Edit>) -> String {
let mut end = text.len();
while end > 0 && is_blank(unit_before(text, end)) {
end -= text[..end].chars().next_back().map_or(1, char::len_utf8);
}
let mut out = String::new();
let mut kept = 0;
let mut quoted = false;
let b = text.as_bytes();
let mut i = 0;
while i < end {
let (c, size) = unit_at(text, i);
if quoted {
if c == 34 {
if b.get(i + 1) == Some(&b'"') {
i += 1;
} else {
quoted = false;
}
}
i += size;
} else if c == 34 {
quoted = true;
i += 1;
} else if is_blank(c) {
let mut j = i + size;
loop {
let (d, n) = unit_at(text, j);
if !is_blank(d) {
break;
}
j += n;
}
let (p, q) = (unit_before(text, i), unit_at(text, j).0);
if i == 0 || q == 10 || q == 13 || (p == 10 || p == 13) && q != 44 {
out.push_str(&text[kept..i]);
edits.push(Edit::cut(i, j));
kept = j;
}
i = j;
} else {
i += size;
}
}
if end < text.len() {
edits.push(Edit::cut(end, text.len()));
}
out.push_str(&text[kept..end]);
out
}
pub(crate) fn prepare_text_for_parsing(text: &str, mut steps: Steps) -> String {
let mut edits = Vec::new();
let rows = trim_rows(text, &mut edits);
let (mut breaks, mut collapsed) = (Vec::new(), String::new());
let mut chars = rows.char_indices().peekable();
while let Some((i, c)) = chars.next() {
if c != '\n' && c != '\r' {
collapsed.push(c);
continue;
}
let mut j = i + 1;
while let Some(&(k, '\n' | '\r')) = chars.peek() {
j = k + 1;
chars.next();
}
breaks.push(Edit {
from: i,
to: j,
text: "\n".into(),
});
collapsed.push('\n');
}
let trimmed = trim(&collapsed);
let lead = collapsed.len() - trim_start(&collapsed).len();
push(&mut steps, edits);
push(&mut steps, breaks);
push(
&mut steps,
vec![
Edit::cut(0, lead),
Edit::cut(lead + trimmed.len(), collapsed.len()),
],
);
trimmed.to_string()
}
fn invalid_xml(s: &str) -> bool {
s.chars().any(|c| {
matches!(c, '\0'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f' | '\u{fffe}' | '\u{ffff}')
})
}
pub(crate) fn parse_sankey(src: &str, db: &mut SankeyDb, source: &str) -> Result<(), Error> {
let t = tokenize(src);
let (types, texts, starts) = (&t.types, &t.texts, &t.starts);
let mut i = 0;
let error = |at: usize, detail: &str| -> Error {
let mut steps = Vec::new();
if source != src {
prepare_text_for_parsing(source, Some(&mut steps));
}
let offset = if at > 0 && at >= src.len() {
let size = src.chars().next_back().map_or(1, char::len_utf8);
input_offset(&steps, src.len() - size) + size
} else {
input_offset(&steps, at)
};
syntax_error(DiagramType::Sankey, source, offset, detail, false)
};
let fail = |i: usize, expected: &str| -> Error {
let kind = types[i];
let got = if kind == T_STUCK {
TOKEN_NAMES[types[i - 1]]
} else {
TOKEN_NAMES[kind]
};
error(starts[i], &format!("Expecting {expected}, got '{got}'"))
};
let expect = |i: &mut usize, kind: usize, also: &str| -> Result<(), Error> {
if types[*i] != kind {
return Err(fail(*i, &format!("'{}'{also}", TOKEN_NAMES[kind])));
}
*i += 1;
Ok(())
};
let field = |i: &mut usize| -> Result<String, Error> {
if types[*i] == T_NON_ESCAPED_TEXT {
*i += 1;
return Ok(texts[*i - 1].to_string());
}
if types[*i] != T_DQUOTE {
return Err(fail(*i, "'DQUOTE', 'NON_ESCAPED_TEXT'"));
}
*i += 1;
if types[*i] != T_ESCAPED_TEXT {
return Err(fail(*i, "'ESCAPED_TEXT'"));
}
let text = texts[*i];
*i += 1;
expect(i, T_DQUOTE, "")?;
Ok(text.to_string())
};
let name = |i: &mut usize| -> Result<String, Error> {
let start = starts[*i];
let text = trim(&field(i)?).replace("\"\"", "\"");
if invalid_xml(&decode_entities(&text)) {
return Err(error(start, "Invalid XML character in node name"));
}
Ok(text)
};
expect(&mut i, T_SANKEY, "")?;
expect(&mut i, T_NEWLINE, "")?;
loop {
let from = name(&mut i)?;
expect(&mut i, T_COMMA, ", 'NEWLINE', 'EOF'")?;
let to = name(&mut i)?;
expect(&mut i, T_COMMA, ", 'NEWLINE', 'EOF'")?;
let start = starts[i];
let value = parse_float(&field(&mut i)?);
if !value.is_finite() {
return Err(error(start, "Expecting a number"));
}
let source = db.find_or_create_node(&from);
let target = db.find_or_create_node(&to);
db.add_link(source, target, value);
if types[i] != T_NEWLINE {
break;
}
i += 1;
}
if types[i] == T_EOF {
i += 1;
}
if types[i] != T_END {
return Err(fail(i, "'NEWLINE', 'EOF'"));
}
Ok(())
}