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;
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))
}
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))
}
#[cfg(test)]
mod tests {
use crate::{parse_mermaid, DiagramType, ThemeType, Layout, Renderer, Theme};
#[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);
}
#[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());
}
}
#[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);
}
#[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);
assert!(output.contains('+') || output.contains('-') || output.contains('|'));
}
#[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();
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();
assert!(diagram.nodes.len() >= 5);
assert!(diagram.edges.len() >= 5);
}
}