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