Skip to main content

graphs_tui/
lib.rs

1#![allow(clippy::too_many_arguments)]
2#![allow(clippy::collapsible_else_if)]
3
4//! graphs-tui - Terminal renderer for Mermaid and D2 diagrams
5//!
6//! # Mermaid Flowchart Example
7//! ```
8//! use graphs_tui::{render_mermaid_to_tui, RenderOptions};
9//!
10//! let input = "flowchart LR\nA[Start] --> B[End]";
11//! let result = render_mermaid_to_tui(input, RenderOptions::default()).unwrap();
12//! println!("{}", result.output);
13//! for warning in &result.warnings {
14//!     eprintln!("Warning: {warning}");
15//! }
16//! ```
17//!
18//! # State Diagram Example
19//! ```
20//! use graphs_tui::{render_state_diagram, RenderOptions};
21//!
22//! let input = "stateDiagram-v2\n    [*] --> Idle\n    Idle --> Running";
23//! let result = render_state_diagram(input, RenderOptions::default()).unwrap();
24//! println!("{}", result.output);
25//! ```
26//!
27//! # Pie Chart Example
28//! ```
29//! use graphs_tui::{render_pie_chart, RenderOptions};
30//!
31//! let input = "pie\n    \"Chrome\" : 65\n    \"Firefox\" : 35";
32//! let result = render_pie_chart(input, RenderOptions::default()).unwrap();
33//! println!("{}", result.output);
34//! ```
35//!
36//! # D2 Example
37//! ```
38//! use graphs_tui::{render_d2_to_tui, RenderOptions};
39//!
40//! let input = "A -> B: connection";
41//! let result = render_d2_to_tui(input, RenderOptions::default()).unwrap();
42//! println!("{}", result.output);
43//! ```
44//!
45//! # Sequence Diagram Example
46//! ```
47//! use graphs_tui::{render_sequence_diagram, RenderOptions};
48//!
49//! let input = "sequenceDiagram\n    Alice->>Bob: Hello\n    Bob-->>Alice: Hi!";
50//! let result = render_sequence_diagram(input, RenderOptions::default()).unwrap();
51//! println!("{}", result.output);
52//! ```
53//!
54//! # Auto-detect Format
55//! ```
56//! use graphs_tui::{render_diagram, RenderOptions};
57//!
58//! let mermaid_input = "flowchart LR\nA --> B";
59//! let d2_input = "A -> B";
60//!
61//! // Automatically detects format
62//! let _ = render_diagram(mermaid_input, RenderOptions::default());
63//! let _ = render_diagram(d2_input, RenderOptions::default());
64//! ```
65
66mod d2_parser;
67mod error;
68mod grid;
69mod layout;
70mod parser;
71mod pathfinding;
72mod pie_parser;
73mod renderer;
74mod seq_parser;
75mod state_parser;
76mod text;
77mod types;
78
79pub use error::MermaidError;
80pub use layout::{compute_layout, compute_layout_with_options};
81pub use types::{
82    DiagramWarning, Direction, Edge, EdgeStyle, Graph, Node, NodeId, NodeShape, RenderOptions,
83    RenderResult, Subgraph, TableField,
84};
85
86use d2_parser::{parse_d2, D2ParseResult};
87use parser::parse_mermaid;
88use pie_parser::{parse_pie_chart as parse_pie, render_pie_chart as render_pie};
89use renderer::render_graph;
90use seq_parser::{parse_sequence_diagram as parse_seq, render_sequence_diagram as render_seq};
91use state_parser::parse_state_diagram;
92
93/// Languages supported by graphs-tui.
94///
95/// Callers can use this instead of maintaining their own hardcoded lists.
96pub const SUPPORTED_LANGUAGES: &[&str] = &["mermaid", "d2"];
97
98/// Check if a language string is supported for rendering.
99///
100/// Matches `SUPPORTED_LANGUAGES` entries case-insensitively.
101pub fn is_supported(lang: &str) -> bool {
102    let lower = lang.to_lowercase();
103    SUPPORTED_LANGUAGES.iter().any(|&l| l == lower)
104}
105
106/// Diagram format
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum DiagramFormat {
109    /// Mermaid flowchart syntax
110    Mermaid,
111    /// Mermaid state diagram
112    StateDiagram,
113    /// Mermaid sequence diagram
114    SequenceDiagram,
115    /// Mermaid pie chart
116    PieChart,
117    /// D2 diagram language
118    D2,
119}
120
121/// Detect the diagram format from input
122pub fn detect_format(input: &str) -> DiagramFormat {
123    let trimmed = input.trim();
124    let lower = trimmed.to_lowercase();
125
126    // Check for specific diagram types first
127    if lower.starts_with("sequencediagram") {
128        return DiagramFormat::SequenceDiagram;
129    }
130    if lower.starts_with("statediagram") {
131        return DiagramFormat::StateDiagram;
132    }
133    if lower.starts_with("pie") {
134        return DiagramFormat::PieChart;
135    }
136
137    // Mermaid flowchart indicators
138    if trimmed.starts_with("flowchart")
139        || trimmed.starts_with("graph ")
140        || trimmed.contains("-->")
141        || trimmed.contains("-.-")
142        || trimmed.contains("==>")
143    {
144        return DiagramFormat::Mermaid;
145    }
146
147    // D2 uses different arrow syntax
148    // D2: ->, <-, <->, --
149    // Mermaid: -->, <--, <-->, ---
150
151    DiagramFormat::D2
152}
153
154/// Unified entry point — render a diagram by language name.
155///
156/// Dispatches to the correct parser based on `lang`:
157/// - `"d2"` → D2 parser
158/// - `"mermaid"` (or any other value) → Mermaid auto-detect (flowchart, state, sequence, pie)
159///
160/// # Example
161/// ```
162/// use graphs_tui::{render, RenderOptions};
163///
164/// let result = render("d2", "A -> B", RenderOptions::default()).unwrap();
165/// println!("{}", result.output);
166/// ```
167pub fn render(
168    lang: &str,
169    code: &str,
170    options: RenderOptions,
171) -> Result<RenderResult, MermaidError> {
172    match lang.to_lowercase().as_str() {
173        "d2" => render_d2_to_tui(code, options),
174        _ => render_diagram(code, options),
175    }
176}
177
178/// Validate diagram without rendering output.
179///
180/// Parses the input and runs layout (for cycle detection) but skips the
181/// expensive grid rendering. Use this for validation-only workflows.
182///
183/// **Note:** This only catches structural warnings (e.g. cycles). Width-dependent
184/// warnings like `LabelDropped` depend on `max_width` which is only applied during
185/// rendering — use `render()` if you need those.
186///
187/// Dispatches by `lang`: `"d2"` → D2 parser, anything else → Mermaid auto-detect.
188///
189/// # Example
190/// ```
191/// use graphs_tui::check;
192///
193/// let warnings = check("mermaid", "flowchart LR\nA --> B\nB --> A").unwrap();
194/// assert!(!warnings.is_empty()); // cycle detected
195/// ```
196pub fn check(lang: &str, code: &str) -> Result<Vec<DiagramWarning>, MermaidError> {
197    match lang.to_lowercase().as_str() {
198        "d2" => {
199            let D2ParseResult {
200                mut graph,
201                mut warnings,
202            } = parse_d2(code)?;
203            warnings.extend(compute_layout(&mut graph));
204            Ok(warnings)
205        }
206        _ => check_mermaid(code),
207    }
208}
209
210/// Validate mermaid input (auto-detect subformat) without rendering.
211fn check_mermaid(code: &str) -> Result<Vec<DiagramWarning>, MermaidError> {
212    let format = detect_format(code);
213    match format {
214        DiagramFormat::D2 => {
215            let D2ParseResult {
216                mut graph,
217                mut warnings,
218            } = parse_d2(code)?;
219            warnings.extend(compute_layout(&mut graph));
220            Ok(warnings)
221        }
222        DiagramFormat::Mermaid => {
223            let mut graph = parse_mermaid(code)?;
224            Ok(compute_layout(&mut graph))
225        }
226        DiagramFormat::StateDiagram => {
227            let mut graph = parse_state_diagram(code)?;
228            Ok(compute_layout(&mut graph))
229        }
230        DiagramFormat::SequenceDiagram => {
231            parse_seq(code)?;
232            Ok(Vec::new())
233        }
234        DiagramFormat::PieChart => {
235            parse_pie(code)?;
236            Ok(Vec::new())
237        }
238    }
239}
240
241/// Render diagram with auto-detection of format
242///
243/// # Arguments
244/// * `input` - Diagram syntax string (Mermaid, State, Pie, or D2)
245/// * `options` - Rendering options
246///
247/// # Returns
248/// * `Ok(RenderResult)` - Rendered diagram with any warnings
249/// * `Err(MermaidError)` - Parse or layout error
250pub fn render_diagram(input: &str, options: RenderOptions) -> Result<RenderResult, MermaidError> {
251    match detect_format(input) {
252        DiagramFormat::Mermaid => render_mermaid_to_tui(input, options),
253        DiagramFormat::StateDiagram => render_state_diagram(input, options),
254        DiagramFormat::SequenceDiagram => render_sequence_diagram(input, options),
255        DiagramFormat::PieChart => render_pie_chart(input, options),
256        DiagramFormat::D2 => render_d2_to_tui(input, options),
257    }
258}
259
260/// Render mermaid flowchart syntax to terminal-displayable text
261///
262/// # Arguments
263/// * `input` - Mermaid flowchart syntax string
264/// * `options` - Rendering options (ASCII mode, max width)
265///
266/// # Returns
267/// * `Ok(RenderResult)` - Rendered diagram with any warnings
268/// * `Err(MermaidError)` - Parse or layout error
269pub fn render_mermaid_to_tui(
270    input: &str,
271    options: RenderOptions,
272) -> Result<RenderResult, MermaidError> {
273    let mut graph = parse_mermaid(input)?;
274    let mut warnings = compute_layout_with_options(&mut graph, &options);
275    Ok(RenderResult {
276        output: render_graph(&graph, &options, &mut warnings),
277        warnings,
278    })
279}
280
281/// Render mermaid state diagram to terminal-displayable text
282///
283/// # Arguments
284/// * `input` - Mermaid state diagram syntax string
285/// * `options` - Rendering options (ASCII mode, max width)
286///
287/// # Returns
288/// * `Ok(RenderResult)` - Rendered diagram with any warnings
289/// * `Err(MermaidError)` - Parse or layout error
290pub fn render_state_diagram(
291    input: &str,
292    options: RenderOptions,
293) -> Result<RenderResult, MermaidError> {
294    let mut graph = parse_state_diagram(input)?;
295    let mut warnings = compute_layout_with_options(&mut graph, &options);
296    Ok(RenderResult {
297        output: render_graph(&graph, &options, &mut warnings),
298        warnings,
299    })
300}
301
302/// Render mermaid pie chart to terminal-displayable text
303///
304/// Pie charts are rendered as horizontal bar charts in terminal.
305///
306/// # Arguments
307/// * `input` - Mermaid pie chart syntax string
308/// * `options` - Rendering options
309///
310/// # Returns
311/// * `Ok(RenderResult)` - Rendered chart with any warnings
312/// * `Err(MermaidError)` - Parse error
313pub fn render_pie_chart(input: &str, options: RenderOptions) -> Result<RenderResult, MermaidError> {
314    let chart = parse_pie(input)?;
315    Ok(RenderResult {
316        output: render_pie(&chart, &options),
317        warnings: Vec::new(),
318    })
319}
320
321/// Render D2 diagram syntax to terminal-displayable text
322///
323/// # Arguments
324/// * `input` - D2 diagram syntax string
325/// * `options` - Rendering options (ASCII mode, max width)
326///
327/// # Returns
328/// * `Ok(RenderResult)` - Rendered diagram with any warnings
329/// * `Err(MermaidError)` - Parse or layout error
330pub fn render_d2_to_tui(input: &str, options: RenderOptions) -> Result<RenderResult, MermaidError> {
331    let D2ParseResult {
332        mut graph,
333        mut warnings,
334    } = parse_d2(input)?;
335    warnings.extend(compute_layout_with_options(&mut graph, &options));
336    Ok(RenderResult {
337        output: render_graph(&graph, &options, &mut warnings),
338        warnings,
339    })
340}
341
342/// Render mermaid sequence diagram to terminal-displayable text
343///
344/// # Arguments
345/// * `input` - Mermaid sequence diagram syntax string
346/// * `options` - Rendering options (ASCII mode, max width)
347///
348/// # Returns
349/// * `Ok(RenderResult)` - Rendered diagram with any warnings
350/// * `Err(MermaidError)` - Parse error
351pub fn render_sequence_diagram(
352    input: &str,
353    options: RenderOptions,
354) -> Result<RenderResult, MermaidError> {
355    let diagram = parse_seq(input)?;
356    Ok(RenderResult {
357        output: render_seq(&diagram, &options),
358        warnings: Vec::new(),
359    })
360}