kitmd 0.1.0

A terminal-based markdown and mermaid renderer/viewer using the Kitty graphics protocol
use super::*;

pub(super) fn compute_block_layout(graph: &Graph, theme: &Theme, config: &LayoutConfig) -> Layout {
    let mut nodes = build_graph_node_layouts(graph, theme, config);

    let node_gap = (theme.font_size * 0.4).max(4.0);
    let column_gap = (theme.font_size * 0.45).max(6.0);
    let origin_x = 6.0;
    let origin_y = 6.0;

    let mut edges: Vec<EdgeLayout> = Vec::new();

    let Some(block) = graph.block.as_ref() else {
        let mut subgraphs = build_subgraph_layouts(graph, &nodes, theme, config);
        normalize_layout(&mut nodes, edges.as_mut_slice(), &mut subgraphs);
        let (max_x, max_y) = bounds_without_padding(&nodes, &subgraphs);
        return Layout {
            kind: graph.kind,
            nodes,
            edges,
            subgraphs,
            width: max_x + 6.0,
            height: max_y + 6.0,
            diagram: DiagramData::Graph {
                state_notes: Vec::new(),
            },
        };
    };

    let (placement_nodes, inferred_columns) = if block.nodes.is_empty() {
        infer_block_grid(graph)
    } else {
        (block.nodes.clone(), 0)
    };
    let columns = block
        .columns
        .filter(|columns| *columns > 0)
        .unwrap_or_else(|| {
            if placement_nodes.is_empty() {
                1
            } else if inferred_columns > 0 {
                inferred_columns
            } else {
                placement_nodes.len().max(1)
            }
        });
    let mut column_widths = vec![0.0f32; columns];
    let mut column_x = vec![0.0f32; columns];
    let mut row_y = Vec::<f32>::new();

    let mut row = 0usize;
    let mut col = 0usize;
    let mut row_heights: Vec<f32> = vec![0.0];

    for node in &placement_nodes {
        if col >= columns {
            col = 0;
            row += 1;
            row_heights.push(0.0);
        }
        let span = node.span.max(1).min(columns);
        if col + span > columns {
            col = 0;
            row += 1;
            row_heights.push(0.0);
        }
        if !node.is_space
            && let Some(layout) = nodes.get(&node.id)
        {
            let per_col = layout.width / span as f32;
            for i in 0..span {
                let idx = col + i;
                if idx < columns {
                    column_widths[idx] = column_widths[idx].max(per_col);
                }
            }
            row_heights[row] = row_heights[row].max(layout.height);
        }
        col += span;
    }

    column_x[0] = origin_x;
    for i in 1..columns {
        column_x[i] = column_x[i - 1] + column_widths[i - 1] + column_gap;
    }

    let mut y_cursor = origin_y;
    for h in &row_heights {
        row_y.push(y_cursor);
        y_cursor += *h + node_gap;
    }

    row = 0;
    col = 0;
    for node in &placement_nodes {
        if col >= columns {
            col = 0;
            row += 1;
        }
        let span = node.span.max(1).min(columns);
        if col + span > columns {
            col = 0;
            row += 1;
        }
        if !node.is_space
            && let Some(layout) = nodes.get_mut(&node.id)
        {
            let start_x = column_x[col];
            let mut span_width = 0.0;
            for i in 0..span {
                let idx = col + i;
                if idx < columns {
                    span_width += column_widths[idx];
                    if i + 1 < span {
                        span_width += column_gap;
                    }
                }
            }
            let x = start_x + (span_width - layout.width) / 2.0;
            let y = row_y[row] + (row_heights[row] - layout.height) / 2.0;
            layout.x = x;
            layout.y = y;
        }
        col += span;
    }

    for edge in &graph.edges {
        let Some(from_layout) = nodes.get(&edge.from) else {
            continue;
        };
        let Some(to_layout) = nodes.get(&edge.to) else {
            continue;
        };
        let from_center = (
            from_layout.x + from_layout.width / 2.0,
            from_layout.y + from_layout.height / 2.0,
        );
        let to_center = (
            to_layout.x + to_layout.width / 2.0,
            to_layout.y + to_layout.height / 2.0,
        );
        let label = edge.label.as_ref().map(|l| measure_label(l, theme, config));
        let start_label = edge
            .start_label
            .as_ref()
            .map(|l| measure_label(l, theme, config));
        let end_label = edge
            .end_label
            .as_ref()
            .map(|l| measure_label(l, theme, config));
        let mut override_style = resolve_edge_style(edges.len(), graph);
        if edge.style == crate::mermaid_engine::ir::EdgeStyle::Dotted
            && override_style.dasharray.is_none()
        {
            override_style.dasharray = Some("3 3".to_string());
        }
        edges.push(EdgeLayout {
            from: edge.from.clone(),
            to: edge.to.clone(),
            label,
            start_label,
            end_label,
            label_anchor: None,
            start_label_anchor: None,
            end_label_anchor: None,
            points: vec![from_center, to_center],
            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,
        });
    }

    let mut subgraphs = build_subgraph_layouts(graph, &nodes, theme, config);
    normalize_layout(&mut nodes, edges.as_mut_slice(), &mut subgraphs);

    let (max_x, max_y) = bounds_with_edges(&nodes, &subgraphs, &edges);
    let width = max_x + 6.0;
    let height = max_y + 6.0;

    Layout {
        kind: graph.kind,
        nodes,
        edges,
        subgraphs,
        width,
        height,
        diagram: DiagramData::Graph {
            state_notes: Vec::new(),
        },
    }
}

fn infer_block_grid(graph: &Graph) -> (Vec<crate::mermaid_engine::ir::BlockNode>, usize) {
    let mut ids: Vec<String> = graph.nodes.keys().cloned().collect();
    ids.sort_by(|a, b| {
        let ao = graph.node_order.get(a).copied().unwrap_or(usize::MAX);
        let bo = graph.node_order.get(b).copied().unwrap_or(usize::MAX);
        ao.cmp(&bo).then_with(|| a.cmp(b))
    });
    if ids.is_empty() {
        return (Vec::new(), 1);
    }

    let mut indegree: HashMap<String, usize> = ids.iter().cloned().map(|id| (id, 0usize)).collect();
    let mut outgoing: HashMap<String, Vec<String>> = HashMap::new();
    for edge in &graph.edges {
        if edge.from == edge.to {
            continue;
        }
        if !indegree.contains_key(&edge.from) || !indegree.contains_key(&edge.to) {
            continue;
        }
        outgoing
            .entry(edge.from.clone())
            .or_default()
            .push(edge.to.clone());
        if let Some(value) = indegree.get_mut(&edge.to) {
            *value += 1;
        }
    }
    for children in outgoing.values_mut() {
        children.sort_by(|a, b| {
            let ao = graph.node_order.get(a).copied().unwrap_or(usize::MAX);
            let bo = graph.node_order.get(b).copied().unwrap_or(usize::MAX);
            ao.cmp(&bo).then_with(|| a.cmp(b))
        });
        children.dedup();
    }

    let mut queue: Vec<String> = ids
        .iter()
        .filter(|id| indegree.get(*id).copied().unwrap_or(0) == 0)
        .cloned()
        .collect();
    let mut rank: HashMap<String, usize> = HashMap::new();
    let mut head = 0usize;
    while head < queue.len() {
        let id = queue[head].clone();
        head += 1;
        let base_rank = rank.get(&id).copied().unwrap_or(0);
        if let Some(children) = outgoing.get(&id) {
            for child in children {
                rank.entry(child.clone())
                    .and_modify(|r| *r = (*r).max(base_rank + 1))
                    .or_insert(base_rank + 1);
                if let Some(value) = indegree.get_mut(child) {
                    *value = value.saturating_sub(1);
                    if *value == 0 {
                        queue.push(child.clone());
                    }
                }
            }
        }
    }

    if rank.len() < ids.len() {
        for id in &ids {
            if rank.contains_key(id) {
                continue;
            }
            let mut inferred_rank = None;
            for edge in &graph.edges {
                if edge.to != *id {
                    continue;
                }
                if let Some(parent_rank) = rank.get(&edge.from).copied() {
                    inferred_rank = Some(
                        inferred_rank.map_or(parent_rank + 1, |r: usize| r.max(parent_rank + 1)),
                    );
                }
            }
            rank.insert(id.clone(), inferred_rank.unwrap_or(0));
        }
    }

    let mut rows: BTreeMap<usize, Vec<String>> = BTreeMap::new();
    for id in ids {
        let row = rank.get(&id).copied().unwrap_or(0);
        rows.entry(row).or_default().push(id);
    }
    for row_ids in rows.values_mut() {
        row_ids.sort_by(|a, b| {
            let ao = graph.node_order.get(a).copied().unwrap_or(usize::MAX);
            let bo = graph.node_order.get(b).copied().unwrap_or(usize::MAX);
            ao.cmp(&bo).then_with(|| a.cmp(b))
        });
    }

    let columns = rows.values().map(Vec::len).max().unwrap_or(1).max(1);
    let mut block_nodes = Vec::new();
    for row_ids in rows.values() {
        for id in row_ids {
            block_nodes.push(crate::mermaid_engine::ir::BlockNode {
                id: id.clone(),
                span: 1,
                is_space: false,
            });
        }
        let missing = columns.saturating_sub(row_ids.len());
        for _ in 0..missing {
            block_nodes.push(crate::mermaid_engine::ir::BlockNode {
                id: "__space".to_string(),
                span: 1,
                is_space: true,
            });
        }
    }
    (block_nodes, columns)
}