use std::collections::HashMap;
use super::members::{ClassMember, MemberType};
use crate::js::chars::{drop_indents, is_digit, trim, trim_start, unit};
use crate::js::map::Ordered;
use crate::text::decode_entities;
use crate::url::sanitize_url;
pub(crate) const AGGREGATION: u8 = 0;
pub(crate) const EXTENSION: u8 = 1;
pub(crate) const COMPOSITION: u8 = 2;
pub(crate) const DEPENDENCY: u8 = 3;
pub(crate) const LOLLIPOP: u8 = 4;
pub(crate) const LINE: u8 = 0;
pub(crate) const DOTTED_LINE: u8 = 1;
pub(crate) type RelationEnd = Option<u8>;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ClassNode {
pub(crate) id: String,
pub(crate) kind: String,
pub(crate) label: String,
pub(crate) css_classes: String,
pub(crate) methods: Vec<ClassMember>,
pub(crate) members: Vec<ClassMember>,
pub(crate) annotations: Vec<String>,
pub(crate) styles: Vec<String>,
pub(crate) parent: Option<String>,
pub(crate) link: Option<String>,
pub(crate) link_target: Option<String>,
pub(crate) have_callback: bool,
pub(crate) tooltip: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ClassRelation {
pub(crate) id1: String,
pub(crate) id2: String,
pub(crate) relation_title1: String,
pub(crate) relation_title2: String,
pub(crate) title: Option<String>,
pub(crate) type1: RelationEnd,
pub(crate) type2: RelationEnd,
pub(crate) line_type: u8,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ClassNote {
pub(crate) id: String,
pub(crate) class: Option<String>,
pub(crate) text: String,
pub(crate) index: usize,
pub(crate) parent: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ClassInterface {
pub(crate) id: String,
pub(crate) label: String,
pub(crate) class_id: String,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct NamespaceNode {
pub(crate) id: String,
pub(crate) label: String,
pub(crate) classes: Vec<String>,
pub(crate) notes: Vec<String>,
pub(crate) children: Vec<String>,
pub(crate) parent: Option<String>,
pub(crate) explicit: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct StyleClass {
pub(crate) id: String,
pub(crate) styles: Vec<String>,
pub(crate) text_styles: Vec<String>,
}
fn set_key(keys: &mut Vec<String>, key: &str) {
if !keys.iter().any(|k| k == key) {
keys.push(key.to_string());
}
}
#[derive(Clone, Debug)]
pub(crate) struct ClassDb {
pub(crate) direction: String,
pub(crate) classes: Ordered<ClassNode>,
pub(crate) relations: Vec<ClassRelation>,
pub(crate) notes: Ordered<ClassNote>,
pub(crate) namespaces: Ordered<NamespaceNode>,
pub(crate) interfaces: Vec<ClassInterface>,
pub(crate) style_classes: Ordered<StyleClass>,
pub(crate) title: Option<String>,
pub(crate) acc_title: Option<String>,
pub(crate) acc_descr: Option<String>,
namespace_stack: Vec<String>,
with_css_class: HashMap<String, Vec<usize>>,
}
impl Default for ClassDb {
fn default() -> Self {
ClassDb {
direction: "TB".to_string(),
classes: Ordered::default(),
relations: Vec::new(),
notes: Ordered::default(),
namespaces: Ordered::default(),
interfaces: Vec::new(),
style_classes: Ordered::default(),
title: None,
acc_title: None,
acc_descr: None,
namespace_stack: Vec::new(),
with_css_class: HashMap::new(),
}
}
}
const DOM_ID_PREFIX: &str = "classId-";
pub(crate) fn split_name(id: &str) -> (&str, &str) {
match id.find('~') {
Some(at) if at > 0 => {
let rest = &id[at + 1..];
let end = rest.find('~').unwrap_or(rest.len());
(&id[..at], &rest[..end])
}
_ => (id, ""),
}
}
fn class_name(id: &str) -> &str {
split_name(id).0
}
fn lookup_id(id: &str) -> String {
if is_digit(unit(id, 0)) {
class_name(&format!("{DOM_ID_PREFIX}{id}")).to_string()
} else {
class_name(id).to_string()
}
}
pub(crate) fn cleanup_label(label: &str) -> String {
trim(label.strip_prefix(':').unwrap_or(label)).to_string()
}
impl ClassDb {
pub(crate) fn add_class(&mut self, id: &str) {
let (name, kind) = split_name(id);
if self.classes.get(name).is_some() {
return;
}
let node = ClassNode {
id: name.to_string(),
kind: kind.to_string(),
label: name.to_string(),
css_classes: "default".to_string(),
methods: Vec::new(),
members: Vec::new(),
annotations: Vec::new(),
styles: Vec::new(),
parent: None,
link: None,
link_target: None,
have_callback: false,
tooltip: None,
};
let at = self.classes.set(name, node);
self.tag(at, "default");
}
fn tag(&mut self, node: usize, css_class: &str) {
let tagged = self
.with_css_class
.entry(css_class.to_string())
.or_default();
if !tagged.contains(&node) {
tagged.push(node);
}
}
fn ensure(&mut self, id: &str) -> &mut ClassNode {
self.add_class(id);
self.classes
.get_mut(class_name(id))
.expect("a class just added")
}
pub(crate) fn set_class_label(&mut self, id: &str, label: &str) {
self.ensure(id).label = label.to_string();
}
pub(crate) fn add_relation(&mut self, mut relation: ClassRelation) {
let (type1, type2) = (relation.type1, relation.type2);
if type1 == Some(LOLLIPOP) && type2.is_none() {
self.add_class(&relation.id2);
relation.id1 = self.add_interface(&relation.id1, &relation.id2);
} else if type2 == Some(LOLLIPOP) && type1.is_none() {
self.add_class(&relation.id1);
relation.id2 = self.add_interface(&relation.id2, &relation.id1);
} else {
self.add_class(&relation.id1);
self.add_class(&relation.id2);
}
relation.id1 = class_name(&relation.id1).to_string();
relation.id2 = class_name(&relation.id2).to_string();
relation.relation_title1 = trim(&relation.relation_title1).to_string();
relation.relation_title2 = trim(&relation.relation_title2).to_string();
self.relations.push(relation);
}
fn add_interface(&mut self, label: &str, class_id: &str) -> String {
let id = format!("interface{}", self.interfaces.len());
self.interfaces.push(ClassInterface {
id: id.clone(),
label: label.to_string(),
class_id: class_id.to_string(),
});
id
}
pub(crate) fn add_annotation(&mut self, class: &str, annotation: &str) {
self.ensure(class).annotations.push(annotation.to_string());
}
pub(crate) fn add_member(&mut self, class: &str, member: &str) {
let node = self.ensure(class);
let text = trim(member);
if text.starts_with("<<") && text.ends_with(">>") {
node.annotations.push(text[2..text.len() - 2].to_string());
} else if text.find(')').is_some_and(|i| i > 0) {
let parsed = ClassMember::new(text, MemberType::Method);
if parsed.member_type == MemberType::Method {
node.methods.push(parsed);
} else {
node.members.push(parsed);
}
} else if !text.is_empty() {
node.members
.push(ClassMember::new(text, MemberType::Attribute));
}
}
pub(crate) fn add_members(&mut self, class: &str, members: &[&str]) {
for m in members {
self.add_member(class, m);
}
}
pub(crate) fn add_note(&mut self, text: &str, class: Option<&str>) -> String {
let index = self.notes.len();
let id = format!("note{index}");
let note = ClassNote {
id: id.clone(),
class: class.map(str::to_string),
text: text.to_string(),
index,
parent: None,
};
self.notes.set(&id, note);
id
}
pub(crate) fn set_css_class(&mut self, ids: &str, css_class: &str) {
for id in ids.split(',') {
let Some(at) = self.classes.index_of(&lookup_id(id)) else {
continue;
};
let node = &mut self.classes[at];
node.css_classes.push(' ');
node.css_classes.push_str(css_class);
for word in css_class.split(' ') {
self.tag(at, word);
}
}
}
pub(crate) fn define_class(&mut self, ids: &[&str], style: &[String]) {
for &id in ids {
let at = match self.style_classes.index_of(id) {
Some(at) => at,
None => self.style_classes.set(
id,
StyleClass {
id: id.to_string(),
styles: Vec::new(),
text_styles: Vec::new(),
},
),
};
let class = &mut self.style_classes[at];
for s in style {
if s.contains("color") {
class.text_styles.push(s.replacen("fill", "bgFill", 1));
}
class.styles.push(s.clone());
}
let Some(tagged) = self.with_css_class.get(id) else {
continue;
};
let parts: Vec<&str> = style.iter().flat_map(|s| s.split(',')).collect();
for &node in tagged {
let styles = &mut self.classes[node].styles;
styles.extend(parts.iter().map(|p| p.to_string()));
}
}
}
pub(crate) fn set_tooltip(&mut self, ids: &str, tooltip: &str) {
for id in ids.split(',') {
if let Some(node) = self.classes.get_mut(class_name(id)) {
node.tooltip = Some(tooltip.to_string());
}
}
}
pub(crate) fn set_link(&mut self, ids: &str, link: &str, target: Option<&str>) {
let url = decode_entities(link);
let url = trim(&url);
for id in ids.split(',') {
if let Some(node) = self.classes.get_mut(&lookup_id(id)) {
node.link = (!url.is_empty()).then(|| sanitize_url(url));
node.link_target = Some(target.unwrap_or("_blank").to_string());
}
}
self.set_css_class(ids, "clickable");
}
pub(crate) fn set_click_event(&mut self, ids: &str) {
for id in ids.split(',') {
if let Some(node) = self.classes.get_mut(class_name(id)) {
node.have_callback = true;
}
}
self.set_css_class(ids, "clickable");
}
pub(crate) fn set_direction(&mut self, dir: &str) {
self.direction = dir.to_string();
}
pub(crate) fn set_acc_title(&mut self, text: &str) {
self.acc_title = Some(trim_start(text).to_string());
}
pub(crate) fn set_acc_description(&mut self, text: &str) {
self.acc_descr = Some(drop_indents(text));
}
pub(crate) fn add_namespace(&mut self, id: &str, label: Option<&str>) -> String {
let label = label.filter(|l| !l.is_empty());
let prefix = self.namespace_stack.last().cloned();
let qualified = match &prefix {
Some(p) => format!("{p}.{id}"),
None => id.to_string(),
};
self.namespace_stack.push(qualified.clone());
if let Some(existing) = self.namespaces.get_mut(&qualified) {
existing.explicit = true;
if let Some(label) = label {
existing.label = label.to_string();
}
return qualified;
}
let mut parent_id = prefix.clone();
let mut from = prefix.map_or(0, |p| p.len() + 1);
loop {
let dot = qualified[from..].find('.').map(|d| from + d);
let leaf = dot.is_none();
let current = &qualified[..dot.unwrap_or(qualified.len())];
match self.namespaces.get_mut(current) {
None => {
let part = &qualified[from..dot.unwrap_or(qualified.len())];
let node = NamespaceNode {
id: current.to_string(),
label: match label {
Some(l) if leaf => l.to_string(),
_ => part.to_string(),
},
classes: Vec::new(),
notes: Vec::new(),
children: Vec::new(),
parent: parent_id.clone(),
explicit: leaf,
};
self.namespaces.set(current, node);
}
Some(node) if leaf => node.explicit = true,
Some(_) => {}
}
if let Some(p) = &parent_id
&& let Some(parent) = self.namespaces.get_mut(p)
{
set_key(&mut parent.children, current);
let node = self
.namespaces
.get_mut(current)
.expect("a namespace just set");
if node.parent.is_none() {
node.parent = parent_id.clone();
}
}
let Some(dot) = dot else {
return qualified;
};
parent_id = Some(current.to_string());
from = dot + 1;
}
}
pub(crate) fn pop_namespace(&mut self) {
self.namespace_stack.pop();
}
pub(crate) fn add_classes_to_namespace(
&mut self,
id: &str,
class_names: &[String],
note_names: &[String],
) {
let Some(at) = self.namespaces.index_of(id) else {
return;
};
for name in class_names {
let name = class_name(name);
let Some(node) = self.classes.get_mut(name) else {
continue;
};
node.parent = Some(id.to_string());
set_key(&mut self.namespaces[at].classes, name);
}
for name in note_names {
let Some(note) = self.notes.get_mut(name) else {
continue;
};
note.parent = Some(id.to_string());
set_key(&mut self.namespaces[at].notes, name);
}
}
pub(crate) fn set_css_style(&mut self, id: &str, styles: &[String]) {
let Some(node) = self.classes.get_mut(id) else {
return;
};
for s in styles {
node.styles.extend(s.split(',').map(str::to_string));
}
}
}