nereid 0.9.0

Source-available noncommercial terminal diagram TUI and MCP server for Mermaid-backed sessions
Documentation
// SPDX-FileCopyrightText: 2026 Bruno Meilick
// SPDX-License-Identifier: LicenseRef-Nereid-FreeUse-NoCopy-NoDerivatives
//
// All rights reserved.
//
// This file is part of Nereid and is proprietary software.
// Unauthorized copying, modification, or distribution is prohibited.

//! Flowchart AST: nodes, directed edges, optional groups, and default edge style.
//!
//! Groups exist for future subgraph support; Mermaid subgraph lines are currently ignored on parse.

use std::collections::BTreeMap;

use super::ids::ObjectId;
use super::symbol::SymbolAnchor;

/// Mutable flowchart diagram content keyed by stable object IDs.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FlowchartAst {
    nodes: BTreeMap<ObjectId, FlowNode>,
    edges: BTreeMap<ObjectId, FlowEdge>,
    default_edge_style: Option<String>,
    groups: BTreeMap<ObjectId, FlowGroup>,
    node_groups: BTreeMap<ObjectId, ObjectId>,
}

impl FlowchartAst {
    /// Nodes keyed by stable object ids (`flow/node`).
    pub fn nodes(&self) -> &BTreeMap<ObjectId, FlowNode> {
        &self.nodes
    }

    /// Mutable node map.
    pub fn nodes_mut(&mut self) -> &mut BTreeMap<ObjectId, FlowNode> {
        &mut self.nodes
    }

    /// Edges keyed by stable object ids (`flow/edge`).
    pub fn edges(&self) -> &BTreeMap<ObjectId, FlowEdge> {
        &self.edges
    }

    /// Mutable edge map.
    pub fn edges_mut(&mut self) -> &mut BTreeMap<ObjectId, FlowEdge> {
        &mut self.edges
    }

    /// Default `linkStyle default` payload when present.
    pub fn default_edge_style(&self) -> Option<&str> {
        self.default_edge_style.as_deref()
    }

    /// Set or clear the default edge style string.
    pub fn set_default_edge_style<T: Into<String>>(&mut self, style: Option<T>) {
        self.default_edge_style = style.map(Into::into);
    }

    /// Subgraph groups (future export; parse currently ignores Mermaid subgraphs).
    pub fn groups(&self) -> &BTreeMap<ObjectId, FlowGroup> {
        &self.groups
    }

    /// Mutable group map.
    pub fn groups_mut(&mut self) -> &mut BTreeMap<ObjectId, FlowGroup> {
        &mut self.groups
    }

    /// Node id → group id membership map.
    pub fn node_groups(&self) -> &BTreeMap<ObjectId, ObjectId> {
        &self.node_groups
    }

    /// Mutable node→group membership.
    pub fn node_groups_mut(&mut self) -> &mut BTreeMap<ObjectId, ObjectId> {
        &mut self.node_groups
    }

    /// Group containing `node_id`, if any.
    pub fn node_group(&self, node_id: &ObjectId) -> Option<&ObjectId> {
        self.node_groups.get(node_id)
    }
}

/// Flowchart node with optional Mermaid id, display label, shape, note, and symbol.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlowNode {
    mermaid_id: Option<String>,
    label: String,
    shape: String,
    note: Option<String>,
    symbol: Option<SymbolAnchor>,
}

impl FlowNode {
    /// Rect-shaped node with label only.
    pub fn new(label: impl Into<String>) -> Self {
        Self {
            mermaid_id: None,
            label: label.into(),
            shape: "rect".to_owned(),
            note: None,
            symbol: None,
        }
    }

    /// Explicit shape and optional Mermaid source id (used for sidecar reconciliation).
    pub fn new_with(
        label: impl Into<String>,
        shape: impl Into<String>,
        mermaid_id: Option<String>,
    ) -> Self {
        Self { mermaid_id, label: label.into(), shape: shape.into(), note: None, symbol: None }
    }

    /// Set Mermaid node id for export/parse round-trips.
    pub fn set_mermaid_id<T: Into<String>>(&mut self, mermaid_id: Option<T>) {
        self.mermaid_id = mermaid_id.map(Into::into);
    }

    /// Replace display label.
    pub fn set_label(&mut self, label: impl Into<String>) {
        self.label = label.into();
    }

    /// Replace shape token (`rect`, `circle`, `diamond`, …).
    pub fn set_shape(&mut self, shape: impl Into<String>) {
        self.shape = shape.into();
    }

    /// Sidecar/UI note; not part of Mermaid export.
    pub fn set_note<T: Into<String>>(&mut self, note: Option<T>) {
        self.note = note.map(Into::into);
    }

    /// Attach or clear a Frigg symbol anchor (sidecar).
    pub fn set_symbol(&mut self, symbol: Option<SymbolAnchor>) {
        self.symbol = symbol;
    }

    /// Mermaid source id when known.
    pub fn mermaid_id(&self) -> Option<&str> {
        self.mermaid_id.as_deref()
    }

    /// Display label.
    pub fn label(&self) -> &str {
        &self.label
    }

    /// Shape token used by layout/render/export.
    pub fn shape(&self) -> &str {
        &self.shape
    }

    /// Sidecar/UI note text.
    pub fn note(&self) -> Option<&str> {
        self.note.as_deref()
    }

    /// Optional Frigg symbol anchor.
    pub fn symbol(&self) -> Option<&SymbolAnchor> {
        self.symbol.as_ref()
    }
}

/// Directed edge between two flow nodes with optional label, connector, and style.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlowEdge {
    from_node_id: ObjectId,
    to_node_id: ObjectId,
    label: Option<String>,
    connector: Option<String>,
    style: Option<String>,
}

/// Subgraph grouping label for clustered flowchart nodes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlowGroup {
    mermaid_id: Option<String>,
    label: String,
}

impl FlowGroup {
    /// Group with display label only.
    pub fn new(label: impl Into<String>) -> Self {
        Self { mermaid_id: None, label: label.into() }
    }

    /// Group with optional Mermaid subgraph id.
    pub fn new_with(label: impl Into<String>, mermaid_id: Option<String>) -> Self {
        Self { mermaid_id, label: label.into() }
    }

    /// Set Mermaid subgraph id.
    pub fn set_mermaid_id<T: Into<String>>(&mut self, mermaid_id: Option<T>) {
        self.mermaid_id = mermaid_id.map(Into::into);
    }

    /// Replace group label.
    pub fn set_label(&mut self, label: impl Into<String>) {
        self.label = label.into();
    }

    /// Mermaid subgraph id when known.
    pub fn mermaid_id(&self) -> Option<&str> {
        self.mermaid_id.as_deref()
    }

    /// Display label.
    pub fn label(&self) -> &str {
        &self.label
    }
}

impl FlowEdge {
    /// Unlabeled edge with default connector/style.
    pub fn new(from_node_id: ObjectId, to_node_id: ObjectId) -> Self {
        Self { from_node_id, to_node_id, label: None, connector: None, style: None }
    }

    /// Edge with optional label and style (connector left unset).
    pub fn new_with(
        from_node_id: ObjectId,
        to_node_id: ObjectId,
        label: Option<String>,
        style: Option<String>,
    ) -> Self {
        Self { from_node_id, to_node_id, label, connector: None, style }
    }

    /// Set or clear edge label text.
    pub fn set_label<T: Into<String>>(&mut self, label: Option<T>) {
        self.label = label.map(Into::into);
    }

    /// Set Mermaid connector token (`-->`, `-.->`, …) for export.
    pub fn set_connector<T: Into<String>>(&mut self, connector: Option<T>) {
        self.connector = connector.map(Into::into);
    }

    /// Set per-edge style (from `linkStyle` or tools).
    pub fn set_style<T: Into<String>>(&mut self, style: Option<T>) {
        self.style = style.map(Into::into);
    }

    /// Source node id.
    pub fn from_node_id(&self) -> &ObjectId {
        &self.from_node_id
    }

    /// Target node id.
    pub fn to_node_id(&self) -> &ObjectId {
        &self.to_node_id
    }

    /// Optional edge label.
    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    /// Optional Mermaid connector token.
    pub fn connector(&self) -> Option<&str> {
        self.connector.as_deref()
    }

    /// Optional edge style payload.
    pub fn style(&self) -> Option<&str> {
        self.style.as_deref()
    }
}

#[cfg(test)]
mod tests {
    use super::{FlowEdge, FlowNode};
    use crate::model::ObjectId;

    #[test]
    fn flow_node_can_be_constructed_and_updated() {
        let mut node = FlowNode::new("Hello");
        assert_eq!(node.mermaid_id(), None);
        assert_eq!(node.label(), "Hello");
        assert_eq!(node.shape(), "rect");
        assert_eq!(node.note(), None);

        node.set_mermaid_id(Some("n1"));
        node.set_label("World");
        node.set_shape("circle");
        node.set_note(Some("invariant"));

        assert_eq!(node.mermaid_id(), Some("n1"));
        assert_eq!(node.label(), "World");
        assert_eq!(node.shape(), "circle");
        assert_eq!(node.note(), Some("invariant"));

        node.set_mermaid_id::<&str>(None);
        assert_eq!(node.mermaid_id(), None);

        node.set_note::<&str>(None);
        assert_eq!(node.note(), None);
    }

    #[test]
    fn flow_node_can_be_constructed_with_explicit_mermaid_fields() {
        let node = FlowNode::new_with("Start", "stadium", Some("start".to_owned()));

        assert_eq!(node.mermaid_id(), Some("start"));
        assert_eq!(node.label(), "Start");
        assert_eq!(node.shape(), "stadium");
    }

    #[test]
    fn flow_edge_can_be_constructed_and_updated() {
        let from = ObjectId::new("n1").expect("from node id");
        let to = ObjectId::new("n2").expect("to node id");
        let mut edge = FlowEdge::new(from.clone(), to.clone());

        assert_eq!(edge.from_node_id(), &from);
        assert_eq!(edge.to_node_id(), &to);
        assert_eq!(edge.label(), None);
        assert_eq!(edge.connector(), None);
        assert_eq!(edge.style(), None);

        edge.set_label(Some("yes"));
        edge.set_connector(Some("-.->"));
        edge.set_style(Some("dashed"));

        assert_eq!(edge.label(), Some("yes"));
        assert_eq!(edge.connector(), Some("-.->"));
        assert_eq!(edge.style(), Some("dashed"));

        edge.set_label::<&str>(None);
        edge.set_connector::<&str>(None);
        edge.set_style::<&str>(None);

        assert_eq!(edge.label(), None);
        assert_eq!(edge.connector(), None);
        assert_eq!(edge.style(), None);
    }

    #[test]
    fn flow_edge_can_be_constructed_with_explicit_mermaid_fields() {
        let from = ObjectId::new("n1").expect("from node id");
        let to = ObjectId::new("n2").expect("to node id");
        let edge = FlowEdge::new_with(
            from.clone(),
            to.clone(),
            Some("maybe".to_owned()),
            Some("thick".to_owned()),
        );

        assert_eq!(edge.from_node_id(), &from);
        assert_eq!(edge.to_node_id(), &to);
        assert_eq!(edge.label(), Some("maybe"));
        assert_eq!(edge.style(), Some("thick"));
    }
}