ratatui-markdown 0.3.3

Markdown rendering, syntax highlighting, collapsible trees, and rich scroll widgets for ratatui
Documentation
use pest::Parser;
use pest_derive::Parser;

use super::types::*;

#[derive(Parser)]
#[grammar = "mermaid/grammar.pest"]
pub struct MermaidParser;

pub fn parse(source: &str) -> Result<MermaidDiagram, String> {
    let pairs = MermaidParser::parse(Rule::file, source)
        .map_err(|e| format!("mermaid parse error: {}", e))?;

    let mut direction = Direction::TopDown;
    let mut nodes = Vec::new();
    let mut edges = Vec::new();
    let mut node_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();

    for pair in pairs {
        if pair.as_rule() == Rule::file {
            for inner in pair.into_inner() {
                if inner.as_rule() == Rule::diagram {
                    for dchild in inner.into_inner() {
                        match dchild.as_rule() {
                            Rule::direction => {
                                direction = match dchild.as_str() {
                                    "TD" | "TB" => Direction::TopDown,
                                    "BT" => Direction::BottomUp,
                                    "LR" => Direction::LeftRight,
                                    "RL" => Direction::RightLeft,
                                    _ => Direction::TopDown,
                                };
                            }
                            Rule::stmts => {
                                for stmt in dchild.into_inner() {
                                    parse_stmt(stmt, &mut nodes, &mut edges, &mut node_map);
                                }
                            }
                            _ => {}
                        }
                    }
                }
            }
        }
    }

    Ok(MermaidDiagram {
        direction,
        nodes,
        edges,
    })
}

fn parse_stmt(
    pair: pest::iterators::Pair<Rule>,
    nodes: &mut Vec<MermaidNode>,
    edges: &mut Vec<MermaidEdge>,
    node_map: &mut std::collections::HashMap<String, usize>,
) {
    for inner in pair.into_inner() {
        if inner.as_rule() == Rule::stmt_inner {
            for inner2 in inner.into_inner() {
                match inner2.as_rule() {
                    Rule::chain => {
                        parse_chain(inner2, nodes, edges, node_map);
                    }
                    Rule::nodedef => {
                        parse_nodedef(inner2, nodes, node_map);
                    }
                    _ => {}
                }
            }
        }
    }
}

fn parse_chain(
    pair: pest::iterators::Pair<Rule>,
    nodes: &mut Vec<MermaidNode>,
    edges: &mut Vec<MermaidEdge>,
    node_map: &mut std::collections::HashMap<String, usize>,
) {
    let mut refs: Vec<(String, Option<String>, Option<NodeShape>)> = Vec::new();
    let mut edge_types: Vec<(EdgeType, Option<String>)> = Vec::new();

    for inner in pair.into_inner() {
        match inner.as_rule() {
            Rule::nref => {
                let (id, label, shape) = parse_nref(inner);
                MermaidDiagram::ensure_node(nodes, node_map, &id, label.as_deref(), shape.clone());
                refs.push((id, label, shape));
            }
            Rule::edge => {
                let (et, lbl) = parse_edge(inner);
                edge_types.push((et, lbl));
            }
            _ => {}
        }
    }

    for i in 0..edge_types.len() {
        if i + 1 < refs.len() {
            let (et, lbl) = &edge_types[i];
            edges.push(MermaidEdge {
                source: refs[i].0.clone(),
                target: refs[i + 1].0.clone(),
                label: lbl.clone(),
                edge_type: et.clone(),
            });
        }
    }
}

fn parse_nodedef(
    pair: pest::iterators::Pair<Rule>,
    nodes: &mut Vec<MermaidNode>,
    node_map: &mut std::collections::HashMap<String, usize>,
) {
    let mut id = String::new();
    let mut label: Option<String> = None;
    let mut shape: Option<NodeShape> = None;

    for inner in pair.into_inner() {
        match inner.as_rule() {
            Rule::nid => {
                id = inner.as_str().to_string();
            }
            Rule::shape => {
                let (s, l) = parse_shape(inner);
                shape = Some(s);
                label = l;
            }
            _ => {}
        }
    }

    MermaidDiagram::ensure_node(nodes, node_map, &id, label.as_deref(), shape);
}

fn parse_nref(pair: pest::iterators::Pair<Rule>) -> (String, Option<String>, Option<NodeShape>) {
    let mut id = String::new();
    let mut label: Option<String> = None;
    let mut shape: Option<NodeShape> = None;

    for inner in pair.into_inner() {
        match inner.as_rule() {
            Rule::nid => {
                id = inner.as_str().to_string();
            }
            Rule::shape => {
                let (s, l) = parse_shape(inner);
                shape = Some(s);
                label = l;
            }
            _ => {}
        }
    }

    (id, label, shape)
}

fn parse_shape(pair: pest::iterators::Pair<Rule>) -> (NodeShape, Option<String>) {
    for inner in pair.into_inner() {
        let text = inner
            .clone()
            .into_inner()
            .next()
            .map(|p| p.as_str().trim().to_string());
        let text = text.filter(|t| !t.is_empty());
        match inner.as_rule() {
            Rule::circ => return (NodeShape::Circle, text),
            Rule::rect => return (NodeShape::Rect, text),
            Rule::rnd => return (NodeShape::Rounded, text),
            Rule::diam => return (NodeShape::Diamond, text),
            _ => {}
        }
    }
    (NodeShape::Rect, None)
}

fn parse_edge(pair: pest::iterators::Pair<Rule>) -> (EdgeType, Option<String>) {
    for inner in pair.into_inner() {
        match inner.as_rule() {
            Rule::arr | Rule::arr_lbl => {
                let label = inner.into_inner().next().map(|p| {
                    p.into_inner()
                        .next()
                        .map(|lp| lp.as_str().trim().to_string())
                        .unwrap_or_default()
                });
                return (EdgeType::Arrow, label.filter(|l| !l.is_empty()));
            }
            Rule::ln | Rule::ln_lbl => {
                let label = inner.into_inner().next().map(|p| {
                    p.into_inner()
                        .next()
                        .map(|lp| lp.as_str().trim().to_string())
                        .unwrap_or_default()
                });
                return (EdgeType::Line, label.filter(|l| !l.is_empty()));
            }
            _ => {}
        }
    }
    (EdgeType::Arrow, None)
}