use std::collections::HashSet;
use crate::detect::DiagramType;
use crate::errors::{Error, ErrorCode};
use crate::js::chars::trim;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct VennSubset {
pub(crate) sets: Vec<String>,
pub(crate) size: f64,
pub(crate) label: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct VennText {
pub(crate) sets: Vec<String>,
pub(crate) id: String,
pub(crate) label: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct VennStyle {
pub(crate) targets: Vec<String>,
pub(crate) styles: Vec<(String, String)>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct VennDb {
pub(crate) title: Option<String>,
pub(crate) subsets: Vec<VennSubset>,
pub(crate) text_nodes: Vec<VennText>,
pub(crate) styles: Vec<VennStyle>,
known: HashSet<String>,
current: Option<Vec<String>>,
indent: bool,
}
pub(crate) trait IndentHost {
fn indent_mode(&self) -> bool;
fn set_indent_mode(&mut self, enabled: bool);
}
impl IndentHost for VennDb {
fn indent_mode(&self) -> bool {
self.indent
}
fn set_indent_mode(&mut self, enabled: bool) {
self.indent = enabled;
}
}
pub(crate) fn normalize_text(text: &str) -> String {
let trimmed = trim(text);
if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') {
return trimmed[1..trimmed.len() - 1].to_string();
}
trimmed.to_string()
}
pub(crate) fn sort16(ids: &mut [String]) {
ids.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
}
fn normalize_ids(ids: &[String]) -> Vec<String> {
let mut out: Vec<String> = ids.iter().map(|id| normalize_text(id)).collect();
sort16(&mut out);
out
}
fn normalized(label: Option<&str>) -> Option<String> {
label.filter(|l| !l.is_empty()).map(normalize_text)
}
fn semantic(message: String) -> Error {
let mut e = Error::new(message, ErrorCode::Semantic);
e.diagram = Some(DiagramType::Venn);
e
}
impl VennDb {
pub(crate) fn new(title: Option<&str>) -> VennDb {
VennDb {
title: title.filter(|t| !t.is_empty()).map(str::to_string),
..VennDb::default()
}
}
pub(crate) fn set_diagram_title(&mut self, title: &str) {
self.title = Some(title.to_string());
}
pub(crate) fn add_subset_data(
&mut self,
ids: &[String],
label: Option<&str>,
size: Option<f64>,
) {
let sets = normalize_ids(ids);
if sets.len() == 1 {
self.known.insert(sets[0].clone());
}
self.current = Some(sets.clone());
let n = ids.len() as f64;
self.subsets.push(VennSubset {
sets,
size: size.unwrap_or(10.0 / (n * n)),
label: normalized(label),
});
}
pub(crate) fn add_text_data(&mut self, ids: &[String], id: &str, label: Option<&str>) {
self.text_nodes.push(VennText {
sets: normalize_ids(ids),
id: normalize_text(id),
label: normalized(label),
});
}
pub(crate) fn add_style_data(&mut self, ids: &[String], data: &[(String, String)]) {
let mut styles: Vec<(String, String)> = Vec::new();
for (key, value) in data {
let value = if value.is_empty() {
value.clone()
} else {
normalize_text(value)
};
set_declaration(&mut styles, key, value);
}
self.styles.push(VennStyle {
targets: normalize_ids(ids),
styles,
});
}
pub(crate) fn validate_union_identifiers(&self, ids: &[String]) -> Result<(), Error> {
let unknown: Vec<String> = ids
.iter()
.map(|id| normalize_text(id))
.filter(|id| !self.known.contains(id))
.collect();
if unknown.is_empty() {
return Ok(());
}
Err(semantic(format!(
"unknown set identifier: {}",
unknown.join(", ")
)))
}
pub(crate) fn current_sets(&self) -> Option<&[String]> {
self.current.as_deref()
}
}
pub(crate) fn set_declaration(list: &mut Vec<(String, String)>, property: &str, value: String) {
match list.iter_mut().find(|(p, _)| p == property) {
Some((_, v)) => *v = value,
None => list.push((property.to_string(), value)),
}
}
pub(crate) fn union_of_one() -> Error {
semantic("union requires multiple identifiers".into())
}
pub(crate) fn text_without_set() -> Error {
semantic("text requires set".into())
}