use std::sync::OnceLock;
use crate::common::tokens::{
ACC_TITLE, END, Pattern, Reader, Table, TokenType, acc_descr_named, acc_descr_value,
acc_title_value, keyword, line_break, match_title, title_value, token, tokenize,
};
use crate::detect::DiagramType;
use crate::errors::Error;
use crate::js::chars::{is_letter, is_word, trim};
pub(crate) const T_KEYWORD: usize = 0;
pub(crate) const T_ACC_DESCR: usize = 1;
pub(crate) const T_ACC_TITLE: usize = 2;
pub(crate) const T_TITLE: usize = 3;
pub(crate) const T_CLASS_ANNOTATION: usize = 4;
pub(crate) const T_ICON_ANNOTATION: usize = 5;
pub(crate) const T_DESC_ANNOTATION: usize = 6;
pub(crate) const T_INDENTATION: usize = 7;
pub(crate) const T_QUOTED_NAME: usize = 8;
pub(crate) const T_BARE_NAME: usize = 12;
fn is_blank(s: &str, i: usize) -> bool {
matches!(s.as_bytes().get(i), Some(b' ' | b'\t'))
}
fn blanks(s: &str, mut i: usize) -> usize {
while is_blank(s, i) {
i += 1;
}
i
}
fn annotation_at(src: &str, at: usize) -> bool {
let rest = &src[at.min(src.len())..];
if rest.starts_with("##") || rest.starts_with("icon(") {
return true;
}
if !rest.starts_with(":::") {
return false;
}
let i = blanks(src, at + 3);
src.as_bytes()
.get(i)
.is_some_and(|&c| is_letter(i32::from(c)) || c == b'_')
}
fn match_bare_name(src: &str, at: usize) -> Option<usize> {
let b = src.as_bytes();
if is_blank(src, at) || matches!(b[at], b'\n' | b'\r' | b'"' | b'\'') {
return None;
}
let rest = &src[at..];
if rest.starts_with(":::") || rest.starts_with("icon(") || rest.starts_with("##") {
return None;
}
let mut i = at + 1;
while i < b.len() {
if b[i] == b'\n' || b[i] == b'\r' {
break;
}
if is_blank(src, i) {
let j = blanks(src, i + 1);
if annotation_at(src, j) {
break;
}
i = j;
} else {
i += 1;
}
}
Some(i)
}
fn after_blanks(s: &str, at: usize, more: impl FnOnce(usize) -> Option<usize>) -> Option<usize> {
let i = blanks(s, at);
if i == at { None } else { more(i) }
}
fn word_run(s: &str, mut i: usize) -> usize {
let b = s.as_bytes();
while i < b.len() && (is_word(i32::from(b[i])) || b[i] == b'-') {
i += 1;
}
i
}
fn class_annotation(s: &str, at: usize) -> Option<usize> {
after_blanks(s, at, |i| {
if !s[i..].starts_with(":::") {
return None;
}
let i = blanks(s, i + 3);
let c = *s.as_bytes().get(i)?;
(is_letter(i32::from(c)) || c == b'_').then(|| word_run(s, i + 1))
})
}
fn icon_annotation(s: &str, at: usize) -> Option<usize> {
after_blanks(s, at, |i| {
if !s[i..].starts_with("icon(") {
return None;
}
let mut i = word_run(s, i + "icon(".len());
if s.as_bytes().get(i) == Some(&b':') {
let j = word_run(s, i + 1);
if j > i + 1 {
i = j;
}
}
(s.as_bytes().get(i) == Some(&b')')).then_some(i + 1)
})
}
fn desc_annotation(s: &str, at: usize) -> Option<usize> {
after_blanks(s, at, |i| {
if !s[i..].starts_with("##") {
return None;
}
let b = s.as_bytes();
let end = (i + 2..b.len())
.find(|&k| b[k] == b'\n' || b[k] == b'\r')
.unwrap_or(b.len());
Some(end)
})
}
fn indentation(s: &str, at: usize) -> Option<usize> {
after_blanks(s, at, Some)
}
fn quoted_name(s: &str, at: usize) -> Option<usize> {
let q = s.as_bytes()[at];
if q != b'"' && q != b'\'' {
return None;
}
let end = s[at + 1..].find(char::from(q))?;
Some(at + 1 + end + 1)
}
fn comment(s: &str, at: usize) -> Option<usize> {
if !s[at..].starts_with("%%") {
return None;
}
Some(s[at..].find('\n').map_or(s.len(), |end| at + end))
}
fn name_rest(s: &str, i: usize) -> bool {
s.as_bytes()
.get(i)
.is_some_and(|&c| c != b'\n' && c != b'\r' && !s[i..].starts_with("%%"))
}
fn title(s: &str, at: usize) -> Option<usize> {
match_title(s, at, Some(name_rest))
}
pub(crate) const TREEVIEW_TOKENS: &[TokenType] = &[
TokenType {
longer: &[T_BARE_NAME],
..keyword("treeView-beta")
},
acc_descr_named("ACC_DESCR"),
ACC_TITLE,
TokenType {
first: Some("\t t"),
..token("TITLE", Pattern::Match(title))
},
TokenType {
first: Some("\t "),
..token("CLASS_ANNOTATION", Pattern::Match(class_annotation))
},
TokenType {
first: Some("\t "),
..token("ICON_ANNOTATION", Pattern::Match(icon_annotation))
},
TokenType {
first: Some("\t "),
..token("DESC_ANNOTATION", Pattern::Match(desc_annotation))
},
TokenType {
first: Some("\t "),
..token("INDENTATION", Pattern::Match(indentation))
},
TokenType {
first: Some("\"'"),
..token("QUOTED_NAME", Pattern::Match(quoted_name))
},
TokenType {
first: Some("\t "),
hidden: true,
..token("WS", Pattern::Match(indentation))
},
TokenType {
first: Some("%"),
hidden: true,
..token("ML_COMMENT", Pattern::Match(comment))
},
TokenType {
first: Some("\r\n"),
hidden: true,
..token("NL", Pattern::Match(line_break))
},
token("BARE_NAME", Pattern::Match(match_bare_name)),
];
pub(crate) fn table() -> &'static Table {
static TABLE: OnceLock<Table> = OnceLock::new();
TABLE.get_or_init(|| Table::new(TREEVIEW_TOKENS))
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct TreeNodeAst {
pub(crate) indent: Option<usize>,
pub(crate) name: String,
pub(crate) class_annotation: Option<String>,
pub(crate) icon_annotation: Option<String>,
pub(crate) desc_annotation: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct TreeViewAst {
pub(crate) title: Option<String>,
pub(crate) acc_title: Option<String>,
pub(crate) acc_descr: Option<String>,
pub(crate) nodes: Vec<TreeNodeAst>,
}
fn trim_blanks_end(text: &str) -> &str {
text.trim_end_matches([' ', '\t'])
}
fn convert_value(rule: usize, input: &str) -> String {
match rule {
T_CLASS_ANNOTATION => trim(&trim(input)[3..]).to_string(),
T_ICON_ANNOTATION => {
let trimmed = trim(input);
trimmed[5..trimmed.len() - 1].to_string()
}
T_DESC_ANNOTATION => trim(&trim(input)[2..]).to_string(),
_ => unreachable!("no annotation"),
}
}
pub(crate) fn parse_tree_view(src: &str) -> Result<TreeViewAst, Error> {
let tokens = tokenize(src, table(), DiagramType::TreeView)?;
let mut r = Reader::new(tokens, table(), DiagramType::TreeView);
let mut ast = TreeViewAst::default();
r.expect(T_KEYWORD)?;
loop {
match r.kind() {
T_ACC_DESCR => ast.acc_descr = Some(acc_descr_value(r.take())),
T_ACC_TITLE => ast.acc_title = Some(acc_title_value(r.take())),
T_TITLE => ast.title = Some(title_value(r.take())),
_ => break,
}
}
while r.kind() != END {
let mut node = TreeNodeAst::default();
if r.kind() == T_INDENTATION {
node.indent = Some(r.take().len());
}
match r.kind() {
T_QUOTED_NAME => {
let text = r.take();
node.name = text[1..text.len() - 1].to_string();
}
T_BARE_NAME => node.name = trim_blanks_end(r.take()).to_string(),
_ => return Err(r.fail("a name")),
}
loop {
let kind = r.kind();
let annotation = match kind {
T_CLASS_ANNOTATION => &mut node.class_annotation,
T_ICON_ANNOTATION => &mut node.icon_annotation,
T_DESC_ANNOTATION => &mut node.desc_annotation,
_ => break,
};
*annotation = Some(convert_value(kind, r.take()));
}
ast.nodes.push(node);
}
Ok(ast)
}