tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use std::sync::Arc;

use super::super::diagram::{ErAttributeSpec, ErEntitySpec, ErRelationSpec, ErSpec, ParsedDiagram};
use super::{MermaidParseError, significant_lines};

pub(crate) fn parse(src: &str) -> Result<ParsedDiagram, MermaidParseError> {
    let lines = significant_lines(src);
    let mut entities = Vec::<ErEntitySpec>::new();
    let mut relations = Vec::new();
    let mut idx = 1usize;
    while idx < lines.len() {
        let (line_no, line) = &lines[idx];
        if let Some(name) = line.strip_suffix('{') {
            let name = name.trim();
            let mut attributes = Vec::new();
            idx += 1;
            while idx < lines.len() && lines[idx].1 != "}" {
                let parts = lines[idx].1.split_whitespace().collect::<Vec<_>>();
                if parts.len() >= 2 {
                    attributes.push(ErAttributeSpec {
                        ty: Arc::from(parts[0]),
                        name: Arc::from(parts[1]),
                        keys: parts[2..].iter().map(|p| Arc::from(*p)).collect(),
                    });
                }
                idx += 1;
            }
            upsert_entity(&mut entities, name)
                .attributes
                .extend(attributes);
        } else if let Some((left_card, right_card, left, right, label)) = relation_parts(line) {
            upsert_entity(&mut entities, left);
            upsert_entity(&mut entities, right);
            relations.push(ErRelationSpec {
                left: Arc::from(left),
                right: Arc::from(right),
                left_cardinality: Arc::from(left_card),
                right_cardinality: Arc::from(right_card),
                label: label.map(Arc::from),
            });
        } else {
            return Err(MermaidParseError::new(
                "expected ER entity or relation",
                Some(*line_no),
            ));
        }
        idx += 1;
    }
    if entities.is_empty() {
        return Err(MermaidParseError::new("empty ER diagram", None));
    }
    Ok(ParsedDiagram::Er(ErSpec {
        entities,
        relations,
    }))
}

fn relation_parts(line: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> {
    let (left, rest) = line.split_once(' ')?;
    let (edge, rest) = rest.trim().split_once(' ')?;
    let (right, label) = rest
        .split_once(':')
        .map(|(right, label)| (right.trim(), Some(label.trim())))
        .unwrap_or_else(|| (rest.trim(), None));
    let (left_card, right_card) = edge.split_once("--")?;
    Some((left_card, right_card, left.trim(), right, label))
}

fn upsert_entity<'a>(entities: &'a mut Vec<ErEntitySpec>, name: &str) -> &'a mut ErEntitySpec {
    if let Some(pos) = entities.iter().position(|e| e.name.as_ref() == name) {
        &mut entities[pos]
    } else {
        entities.push(ErEntitySpec {
            name: Arc::from(name),
            attributes: Vec::new(),
        });
        entities.last_mut().unwrap()
    }
}