meraid 0.1.0

Render Mermaid diagrams in your terminal - A Rust implementation
Documentation
//! Meraid - Render Mermaid diagrams in your terminal
//!
//! A Rust implementation for rendering Mermaid diagrams in the terminal.

pub mod diagram;
pub mod parser;
pub mod layout;
pub mod render;
pub mod theme;

pub use diagram::{Diagram, DiagramType, Node, Edge, NodeShape, EdgeStyle};
pub use parser::parse_mermaid;
pub use layout::Layout;
pub use render::Renderer;
pub use theme::{Theme, ThemeType};

use anyhow::Result;

/// Render Mermaid diagram to terminal string
pub fn render(source: &str, theme_type: ThemeType) -> Result<String> {
    let diagram = parse_mermaid(source)?;
    let layout = Layout::new(&diagram).layout();
    let theme = Theme::get(theme_type);
    let renderer = Renderer::new(theme);
    Ok(renderer.render(&diagram, &layout))
}

/// Render Mermaid diagram with custom theme
pub fn render_with_theme(source: &str, theme: Theme) -> Result<String> {
    let diagram = parse_mermaid(source)?;
    let layout = Layout::new(&diagram).layout();
    let renderer = Renderer::new(theme);
    Ok(renderer.render(&diagram, &layout))
}

// ==================== Tests ====================

#[cfg(test)]
mod tests {
    use crate::{parse_mermaid, DiagramType, ThemeType, Layout, Renderer, Theme};
    
    // ==================== Parser Tests ====================
    
    #[test]
    fn test_parse_flowchart_basic() {
        let source = r#"
graph LR
A --> B
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::Flowchart);
        assert_eq!(diagram.direction, "LR");
    }
    
    #[test]
    fn test_parse_flowchart_multiple_edges() {
        let source = r#"
graph LR
A --> B --> C
A --> D
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.nodes.len(), 4);
        assert_eq!(diagram.edges.len(), 3);
    }
    
    #[test]
    fn test_parse_flowchart_chained() {
        let source = "graph LR\nA --> B --> C --> D";
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.edges.len(), 3);
    }
    
    #[test]
    fn test_parse_flowchart_thick_arrow() {
        let source = "graph LR\nA ==> B";
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.edges.len(), 1);
    }
    
    #[test]
    fn test_parse_flowchart_dotted_arrow() {
        let source = "graph LR\nA -.-> B";
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.edges.len(), 1);
    }
    
    #[test]
    fn test_parse_sequence_diagram() {
        let source = r#"
sequenceDiagram
Alice->>Bob: Hello
Bob-->>Alice: Hi
Alice->>Bob: How are you?
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::Sequence);
        assert!(diagram.participants.contains(&"Alice".to_string()));
        assert!(diagram.participants.contains(&"Bob".to_string()));
        assert_eq!(diagram.edges.len(), 3);
    }
    
    #[test]
    fn test_parse_sequence_participants() {
        let source = r#"
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice->>Bob: Hello
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.participants.len(), 3);
    }
    
    #[test]
    fn test_parse_class_diagram() {
        let source = r#"
classDiagram
class Animal {
    +String name
    +int age
}
class Dog {
    +String breed
}
Animal <|-- Dog
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::Class);
    }
    
    #[test]
    fn test_parse_state_diagram() {
        let source = r#"
stateDiagram-v2
[*] --> Idle
Idle --> Processing: start
Processing --> Done: complete
Done --> [*]
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::State);
    }
    
    #[test]
    fn test_parse_pie_chart() {
        let source = r#"
pie title Pets
"Dogs" : 386
"Cats" : 85
"Rats" : 15
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::Pie);
        assert_eq!(diagram.nodes.len(), 3);
    }
    
    #[test]
    fn test_parse_unknown_defaults_to_flowchart() {
        let source = "A --> B";
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.diagram_type, DiagramType::Flowchart);
    }
    
    #[test]
    fn test_parse_flowchart_vertical_direction() {
        let source = "graph TB\nA --> B";
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.direction, "TB");
    }
    
    #[test]
    fn test_parse_comments_ignored() {
        let source = r#"
%% This is a comment
graph LR
%% Another comment
A --> B
%% End comment
"#;
        let diagram = parse_mermaid(source).unwrap();
        assert_eq!(diagram.edges.len(), 1);
    }
    
    // ==================== Integration Tests ====================
    
    #[test]
    fn test_full_render_flowchart() {
        let source = "graph LR\nA --> B";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);
        
        assert!(!output.is_empty());
    }
    
    #[test]
    fn test_full_render_sequence() {
        let source = "sequenceDiagram\nAlice->>Bob: Hello";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);
        
        assert!(!output.is_empty());
    }
    
    #[test]
    fn test_full_render_pie() {
        let source = "pie title Test\nA : 50\nB : 50";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);
        
        assert!(!output.is_empty());
    }

    #[test]
    fn test_render_class_diagram_with_chinese_alignment() {
        let source = r#"
classDiagram
class 用户服务 {
    +获取用户
    +更新资料
}
"#;
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);

        assert!(output.contains("│    用户服务    │"));
        assert!(output.contains("│+获取用户       │"));
        assert!(output.contains("│+更新资料       │"));
    }

    #[test]
    fn test_full_render_flowchart_with_chinese_label_keeps_borders_aligned() {
        let source = "graph LR\n开始 --> 结束";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);

        assert!(output.contains("┌──────────┐"));
        assert!(output.contains("│   开始   │"));
        assert!(output.contains("└──────────┘"));
    }
    
    #[test]
    fn test_sequence_diagram_with_chinese_and_mixed_text_alignment() {
        let source = r#"
sequenceDiagram
participant 用户A
participant API服务
用户A->>API服务: 查询 user-详情
API服务-->>用户A: 返回 成功OK
"#;
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);

        assert!(output.contains("用户A"));
        assert!(output.contains("API服务"));
        assert!(output.contains(""));
        assert!(output.contains("├─────────────────▶ 查询 user-详情"));
        assert!(output.contains("◀───────────────────────────────────┤ 返回 成功OK"));
    }

    #[test]
    fn test_state_diagram_with_chinese_and_mixed_text() {
        let source = r#"
stateDiagram-v2
[*] --> 待处理
待处理 --> 处理中: 开始 job-1
处理中 --> 已完成: 完成 OK
已完成 --> [*]
"#;
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        let renderer = Renderer::new(theme);
        let output = renderer.render(&diagram, &layout);

        assert!(output.contains("待处理 ──▶ 处理中 : 开始 job-1"));
        assert!(output.contains("处理中 ──▶ 已完成 : 完成 OK"));
    }

    #[test]
    fn test_all_themes() {
        let source = "graph LR\nA --> B";
        let diagram = parse_mermaid(source).unwrap();
        
        for theme_type in [
            ThemeType::Default,
            ThemeType::Terra,
            ThemeType::Neon,
            ThemeType::Mono,
            ThemeType::Amber,
            ThemeType::Phosphor,
        ] {
            let theme = Theme::get(theme_type);
            let layout = Layout::new(&diagram).layout();
            let renderer = Renderer::new(theme);
            let output = renderer.render(&diagram, &layout);
            assert!(!output.is_empty());
        }
    }
    
    // ==================== Layout Tests ====================
    
    #[test]
    fn test_flowchart_layout_has_positions() {
        let source = "graph LR\nA --> B --> C";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        
        assert!(!layout.positions.is_empty());
        assert!(layout.width > 0);
        assert!(layout.height > 0);
    }
    
    #[test]
    fn test_layout_with_many_nodes() {
        let source = "graph LR\nA --> B --> C --> D --> E --> F --> G --> H";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        
        assert_eq!(layout.positions.len(), 8);
    }
    
    // ==================== Render Tests ====================
    
    #[test]
    fn test_renderer_respects_ascii_only() {
        let source = "graph LR\nA --> B";
        let diagram = parse_mermaid(source).unwrap();
        let layout = Layout::new(&diagram).layout();
        let theme = Theme::get(ThemeType::Default);
        
        let renderer = Renderer::new(theme).ascii_only(true);
        let output = renderer.render(&diagram, &layout);
        
        // ASCII mode should use +, -, |
        assert!(output.contains('+') || output.contains('-') || output.contains('|'));
    }
    
    // ==================== Edge Cases ====================
    
    #[test]
    fn test_empty_source() {
        let result = parse_mermaid("");
        assert!(result.is_ok());
    }
    
    #[test]
    fn test_only_comments() {
        let source = "%% comment only";
        let diagram = parse_mermaid(source).unwrap();
        // Should default to flowchart with no nodes
        assert_eq!(diagram.diagram_type, DiagramType::Flowchart);
    }
    
    #[test]
    fn test_complex_flow() {
        let source = r#"
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Process]
B -->|No| D[Skip]
C --> E[End]
D --> E
"#;
        let diagram = parse_mermaid(source).unwrap();
        // Due to how parsing works, each labeled node (A[Start], B{Decision}, etc.) 
        // might create multiple entries. Just check we have nodes and edges.
        assert!(diagram.nodes.len() >= 5);
        assert!(diagram.edges.len() >= 5);
    }
}