kitmd 0.2.1

A terminal-based markdown and mermaid renderer/viewer using the Kitty graphics protocol
use std::collections::BTreeMap;

use crate::mermaid_engine::config::LayoutConfig;
use crate::mermaid_engine::ir::{DiagramKind, Graph};

use super::super::{EdgeLayout, NodeLayout, TextBlock, resolve_edge_style};
use super::path_cleanup::{
    deoverlap_flowchart_paths, detour_flowchart_paths_around_non_endpoint_nodes,
    reduce_orthogonal_path_crossings, simplify_flowchart_axis_oscillations,
    simplify_flowchart_detour_rectangles,
};

pub(in crate::mermaid_engine::layout) fn apply_edge_path_cleanup(
    graph: &Graph,
    nodes: &BTreeMap<String, NodeLayout>,
    routed_points: &mut [Vec<(f32, f32)>],
    config: &LayoutConfig,
) {
    if graph.kind == DiagramKind::Flowchart {
        reduce_orthogonal_path_crossings(graph, nodes, routed_points, config);
        deoverlap_flowchart_paths(graph, nodes, routed_points, config);
        simplify_flowchart_detour_rectangles(graph, nodes, routed_points);
        simplify_flowchart_axis_oscillations(routed_points);
        detour_flowchart_paths_around_non_endpoint_nodes(graph, nodes, routed_points, config);
        simplify_flowchart_axis_oscillations(routed_points);
    } else if matches!(
        graph.kind,
        DiagramKind::Class | DiagramKind::Er | DiagramKind::State
    ) {
        reduce_orthogonal_path_crossings(graph, nodes, routed_points, config);
        if graph.kind == DiagramKind::Er {
            deoverlap_flowchart_paths(graph, nodes, routed_points, config);
        }
    }
}

pub(in crate::mermaid_engine::layout) fn build_edge_layouts(
    graph: &Graph,
    routed_points: &[Vec<(f32, f32)>],
    edge_route_labels: &[Option<TextBlock>],
    edge_start_labels: &[Option<TextBlock>],
    edge_end_labels: &[Option<TextBlock>],
    label_anchors: &[Option<(f32, f32)>],
    config: &LayoutConfig,
) -> Vec<EdgeLayout> {
    let mut edges = Vec::with_capacity(graph.edges.len());
    for (idx, edge) in graph.edges.iter().enumerate() {
        let label = edge_route_labels[idx].clone();
        let start_label = edge_start_labels[idx].clone();
        let end_label = edge_end_labels[idx].clone();
        let mut override_style = resolve_edge_style(idx, graph);
        if graph.kind == DiagramKind::Requirement {
            let is_contains = edge.label.as_deref() == Some("contains");
            if override_style.stroke.is_none() {
                override_style.stroke = Some(config.requirement.edge_stroke.clone());
            }
            override_style.stroke_width = Some(
                override_style
                    .stroke_width
                    .unwrap_or(config.requirement.edge_stroke_width),
            );
            if !is_contains && override_style.dasharray.is_none() {
                override_style.dasharray = Some(config.requirement.edge_dasharray.clone());
            }
            if override_style.label_color.is_none() {
                override_style.label_color = Some(config.requirement.edge_label_color.clone());
            }
        }
        edges.push(EdgeLayout {
            from: edge.from.clone(),
            to: edge.to.clone(),
            label,
            start_label,
            end_label,
            points: routed_points[idx].clone(),
            directed: edge.directed,
            arrow_start: edge.arrow_start,
            arrow_end: edge.arrow_end,
            arrow_start_kind: edge.arrow_start_kind,
            arrow_end_kind: edge.arrow_end_kind,
            start_decoration: edge.start_decoration,
            end_decoration: edge.end_decoration,
            style: edge.style,
            override_style,
            label_anchor: label_anchors[idx],
            start_label_anchor: None,
            end_label_anchor: None,
        });
    }
    edges
}

#[cfg(all(test, feature = "mermaid_engine_internal_tests"))]
mod tests {
    use super::build_edge_layouts;
    use crate::mermaid_engine::config::LayoutConfig;
    use crate::mermaid_engine::ir::{DiagramKind, EdgeStyle, Graph, NodeShape};
    use crate::mermaid_engine::layout::TextBlock;

    #[test]
    fn build_edge_layouts_applies_requirement_defaults() {
        let mut graph = Graph::new();
        graph.kind = DiagramKind::Requirement;
        graph.ensure_node("A", Some("A".to_string()), Some(NodeShape::Rectangle));
        graph.ensure_node("B", Some("B".to_string()), Some(NodeShape::Rectangle));
        graph.edges.push(crate::mermaid_engine::ir::Edge {
            from: "A".to_string(),
            to: "B".to_string(),
            label: Some("requires".to_string()),
            start_label: None,
            end_label: None,
            directed: true,
            arrow_start: false,
            arrow_end: true,
            arrow_start_kind: None,
            arrow_end_kind: None,
            start_decoration: None,
            end_decoration: None,
            style: EdgeStyle::Solid,
        });

        let config = LayoutConfig::default();
        let edges = build_edge_layouts(
            &graph,
            &[vec![(0.0, 0.0), (10.0, 0.0)]],
            &[Some(TextBlock {
                lines: vec!["requires".to_string()],
                width: 30.0,
                height: 10.0,
            })],
            &[None],
            &[None],
            &[Some((5.0, 0.0))],
            &config,
        );

        assert_eq!(
            edges[0].override_style.stroke.as_deref(),
            Some(config.requirement.edge_stroke.as_str())
        );
        assert_eq!(
            edges[0].override_style.stroke_width,
            Some(config.requirement.edge_stroke_width)
        );
        assert_eq!(
            edges[0].override_style.label_color.as_deref(),
            Some(config.requirement.edge_label_color.as_str())
        );
    }

    #[test]
    fn build_edge_layouts_keeps_requirement_contains_solid() {
        let mut graph = Graph::new();
        graph.kind = DiagramKind::Requirement;
        graph.ensure_node("A", Some("A".to_string()), Some(NodeShape::Rectangle));
        graph.ensure_node("B", Some("B".to_string()), Some(NodeShape::Rectangle));
        graph.edges.push(crate::mermaid_engine::ir::Edge {
            from: "A".to_string(),
            to: "B".to_string(),
            label: Some("contains".to_string()),
            start_label: None,
            end_label: None,
            directed: true,
            arrow_start: true,
            arrow_end: false,
            arrow_start_kind: None,
            arrow_end_kind: None,
            start_decoration: None,
            end_decoration: None,
            style: EdgeStyle::Solid,
        });

        let config = LayoutConfig::default();
        let edges = build_edge_layouts(
            &graph,
            &[vec![(0.0, 0.0), (10.0, 0.0)]],
            &[Some(TextBlock {
                lines: vec!["<<contains>>".to_string()],
                width: 70.0,
                height: 10.0,
            })],
            &[None],
            &[None],
            &[Some((5.0, 0.0))],
            &config,
        );

        assert_eq!(edges[0].override_style.dasharray, None);
    }
}