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}