mmdflux 2.4.1

Render Mermaid diagrams as Unicode text, ASCII, SVG, and MMDS JSON.
Documentation
use std::cell::RefCell;

use super::graph::LayoutGraph;
use super::pipeline::layout_with_trace;
use super::types::DummyType;
use super::{DiGraph, LayoutConfig, NodeId};

thread_local! {
    static ACTIVE_TRACE: RefCell<Option<LayeredPhaseTrace>> = const { RefCell::new(None) };
}

#[derive(Debug, Default, Clone)]
pub(crate) struct LayeredPhaseTrace {
    pub(crate) stages: Vec<TraceStageSnapshot>,
}

impl LayeredPhaseTrace {
    pub(crate) fn push_stage(&mut self, stage: TraceStageSnapshot) {
        self.stages.push(stage);
    }

    pub(crate) fn has_stage(&self, stage: TraceStage) -> bool {
        self.stages.iter().any(|snapshot| snapshot.stage == stage)
    }

    pub(crate) fn stage_names(&self) -> Vec<TraceStage> {
        self.stages.iter().map(|stage| stage.stage).collect()
    }

    pub(crate) fn generated_dummy_ids(&self) -> Vec<String> {
        self.stages
            .iter()
            .flat_map(|stage| stage.dummies.iter().map(|dummy| dummy.generated_id.clone()))
            .collect()
    }
}

#[derive(Debug, Clone)]
pub(crate) struct TraceStageSnapshot {
    pub(crate) stage: TraceStage,
    pub(crate) nodes: Vec<TraceNodeSnapshot>,
    pub(crate) dummies: Vec<TraceDummySnapshot>,
    pub(crate) reversed_edges: Vec<usize>,
}

impl TraceStageSnapshot {
    pub(crate) fn empty(stage: TraceStage) -> Self {
        Self {
            stage,
            nodes: Vec::new(),
            dummies: Vec::new(),
            reversed_edges: Vec::new(),
        }
    }

    pub(crate) fn from_layout_graph(stage: TraceStage, graph: &LayoutGraph) -> Self {
        let nodes = graph
            .node_ids
            .iter()
            .enumerate()
            .filter(|(idx, _)| !graph.is_dummy_index(*idx))
            .map(|(idx, id)| TraceNodeSnapshot {
                id: id.0.clone(),
                rank: graph.ranks[idx],
                order: graph.order[idx],
                x: Some(graph.positions[idx].x),
                y: Some(graph.positions[idx].y),
            })
            .collect();

        let mut dummies = Vec::new();
        for chain in &graph.dummy_chains {
            for (chain_position, dummy_id) in chain.dummy_ids.iter().enumerate() {
                let Some(dummy) = graph.dummy_nodes.get(dummy_id) else {
                    continue;
                };
                let Some(&idx) = graph.node_index.get(dummy_id) else {
                    continue;
                };
                let is_label_dummy = chain.label_dummy_index == Some(chain_position);
                dummies.push(TraceDummySnapshot {
                    generated_id: dummy_id.0.clone(),
                    key: DummyTraceKey {
                        edge_index: chain.edge_index,
                        role: DummyTraceRole::from(dummy.dummy_type),
                        chain_position,
                        is_label_dummy,
                    },
                    rank: graph.ranks[idx],
                    order: graph.order[idx],
                });
            }
        }

        Self {
            stage,
            nodes,
            dummies,
            reversed_edges: graph.reversed_edges.iter().copied().collect(),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum TraceStage {
    Acyclic,
    Rank,
    Normalize,
    Order,
    Position,
    Route,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TraceNodeSnapshot {
    pub(crate) id: String,
    pub(crate) rank: i32,
    pub(crate) order: usize,
    pub(crate) x: Option<f64>,
    pub(crate) y: Option<f64>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TraceDummySnapshot {
    pub(crate) generated_id: String,
    pub(crate) key: DummyTraceKey,
    pub(crate) rank: i32,
    pub(crate) order: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct DummyTraceKey {
    pub(crate) edge_index: usize,
    pub(crate) role: DummyTraceRole,
    pub(crate) chain_position: usize,
    pub(crate) is_label_dummy: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum DummyTraceRole {
    Edge,
    EdgeLabel,
}

impl From<DummyType> for DummyTraceRole {
    fn from(value: DummyType) -> Self {
        match value {
            DummyType::Edge => Self::Edge,
            DummyType::EdgeLabel => Self::EdgeLabel,
        }
    }
}

pub(crate) fn begin_capture() {
    ACTIVE_TRACE.with(|trace| {
        *trace.borrow_mut() = Some(LayeredPhaseTrace::default());
    });
}

pub(crate) fn finish_capture() -> LayeredPhaseTrace {
    ACTIVE_TRACE.with(|trace| trace.borrow_mut().take().unwrap_or_default())
}

pub(crate) fn capture_stage(stage: TraceStage, graph: &LayoutGraph) {
    ACTIVE_TRACE.with(|trace| {
        if let Some(active) = trace.borrow_mut().as_mut() {
            active.push_stage(TraceStageSnapshot::from_layout_graph(stage, graph));
        }
    });
}

#[test]
fn dummy_chain_key_ignores_generated_dummy_node_id() {
    let before = TraceDummySnapshot {
        generated_id: "_d4".to_string(),
        key: DummyTraceKey {
            edge_index: 7,
            role: DummyTraceRole::Edge,
            chain_position: 2,
            is_label_dummy: false,
        },
        rank: 3,
        order: 1,
    };
    let after = TraceDummySnapshot {
        generated_id: "_d9".to_string(),
        key: before.key.clone(),
        rank: 3,
        order: 1,
    };

    assert_eq!(before.key, after.key);
    assert_ne!(before.generated_id, after.generated_id);
}

#[test]
fn phase_trace_records_named_stages_in_order() {
    let mut trace = LayeredPhaseTrace::default();
    trace.push_stage(TraceStageSnapshot::empty(TraceStage::Acyclic));
    trace.push_stage(TraceStageSnapshot::empty(TraceStage::Rank));
    trace.push_stage(TraceStageSnapshot::empty(TraceStage::Normalize));

    assert_eq!(
        trace.stage_names(),
        vec![TraceStage::Acyclic, TraceStage::Rank, TraceStage::Normalize]
    );
}

#[test]
fn stage_snapshot_carries_future_diff_payloads() {
    let snapshot = TraceStageSnapshot {
        stage: TraceStage::Order,
        nodes: vec![TraceNodeSnapshot {
            id: "A".to_string(),
            rank: 1,
            order: 0,
            x: Some(10.0),
            y: Some(20.0),
        }],
        dummies: vec![TraceDummySnapshot {
            generated_id: "_ld2".to_string(),
            key: DummyTraceKey {
                edge_index: 4,
                role: DummyTraceRole::EdgeLabel,
                chain_position: 1,
                is_label_dummy: true,
            },
            rank: 2,
            order: 3,
        }],
        reversed_edges: vec![4],
    };

    assert_eq!(snapshot.stage, TraceStage::Order);
    assert_eq!(snapshot.nodes.len(), 1);
    assert_eq!(snapshot.dummies.len(), 1);
    assert_eq!(snapshot.reversed_edges, vec![4]);
    assert_eq!([TraceStage::Position, TraceStage::Route].len(), 2);
}

#[test]
fn trace_capture_includes_core_layered_stages() {
    let mut graph = DiGraph::<()>::new();
    graph.add_node(NodeId::from("A"), ());
    graph.add_node(NodeId::from("B"), ());
    graph.add_edge(NodeId::from("A"), NodeId::from("B"));

    let (_layout, trace) =
        layout_with_trace(&graph, &LayoutConfig::default(), |_id, _node| (40.0, 20.0));

    assert!(trace.has_stage(TraceStage::Acyclic));
    assert!(trace.has_stage(TraceStage::Rank));
    assert!(trace.has_stage(TraceStage::Normalize));
    assert!(trace.has_stage(TraceStage::Order));
    assert!(trace.has_stage(TraceStage::Position));
    assert!(trace.has_stage(TraceStage::Route));
}