Skip to main content

ariel_rs/
lib.rs

1//! `ariel-rs` — a pure-Rust Mermaid diagram renderer.
2//!
3//! Converts [Mermaid](https://mermaid.js.org/) diagram source text into SVG
4//! strings without requiring a JavaScript runtime or browser.
5//!
6//! # Quick start
7//!
8//! ```
9//! let svg = ariel_rs::render("graph LR\n  A --> B", ariel_rs::theme::Theme::Default);
10//! assert!(svg.contains("<svg"));
11//! ```
12//!
13//! The two entry points are [`render`] and [`render_svg`] (an alias).  Both
14//! accept any supported Mermaid diagram type and a [`theme::Theme`] variant.
15//! For a fallible variant that returns [`RenderError`] instead of an error SVG,
16//! use [`try_render`].
17#![deny(missing_docs)]
18pub(crate) mod diagrams;
19/// Error and parse-result types for ariel-rs diagram parsing and rendering.
20///
21/// Key types exported here are [`ParseError`], [`ParseResult`], and
22/// [`RenderError`], which are also re-exported at the crate root.
23pub mod error;
24pub(crate) mod error_svg;
25pub(crate) mod icons;
26pub(crate) mod style;
27pub(crate) mod svg;
28pub(crate) mod text;
29pub(crate) mod text_browser_metrics;
30/// Colour-theme types for Mermaid diagram rendering.
31///
32/// The two public types here are [`Theme`](theme::Theme), which you pass to
33/// [`render`] / [`render_svg`], and [`ThemeVars`](theme::ThemeVars), which
34/// holds the resolved colour and font values.
35pub mod theme;
36
37pub use error::{ParseError, ParseResult, RenderError};
38
39/// Per-call rendering configuration passed to [`render_with_options`] and
40/// [`try_render_with_options`].
41///
42/// Use [`RenderOptions::default()`] to get a zero-configuration value and then
43/// set individual fields as needed.
44///
45/// # Examples
46///
47/// ```
48/// use ariel_rs::{RenderOptions, theme::Theme};
49///
50/// let opts = RenderOptions {
51///     theme: Theme::Dark,
52///     font_family: Some("monospace".to_string()),
53///     ..RenderOptions::default()
54/// };
55/// ```
56pub struct RenderOptions {
57    /// The colour theme to apply to the rendered diagram.
58    pub theme: theme::Theme,
59    /// Optional CSS font-family string (e.g. `"sans-serif"`).
60    ///
61    /// When `None` the renderer uses its built-in default.
62    /// Support for this field in individual renderers is planned for a future
63    /// release.
64    pub font_family: Option<String>,
65    /// Optional base font size in points.
66    ///
67    /// When `None` the renderer uses its built-in default.
68    /// Support for this field in individual renderers is planned for a future
69    /// release.
70    pub font_size: Option<f64>,
71    /// Optional maximum width of the output SVG in pixels.
72    ///
73    /// When `None` the renderer uses its built-in default.
74    /// Support for this field in individual renderers is planned for a future
75    /// release.
76    pub max_width: Option<f64>,
77    /// Optional background colour as a CSS colour string (e.g. `"#ffffff"`).
78    ///
79    /// When `None` the renderer uses the theme's default background.
80    /// Support for this field in individual renderers is planned for a future
81    /// release.
82    pub background: Option<String>,
83}
84
85impl Default for RenderOptions {
86    fn default() -> Self {
87        Self {
88            theme: theme::Theme::Default,
89            font_family: None,
90            font_size: None,
91            max_width: None,
92            background: None,
93        }
94    }
95}
96
97use std::any::Any;
98use std::panic::{self, AssertUnwindSafe};
99
100/// The type of a Mermaid diagram, as detected from the source text.
101///
102/// Returned by [`detect`]. Use this to inspect which diagram kind a source
103/// string represents before (or instead of) rendering it.
104///
105/// This enum is `#[non_exhaustive]`: new diagram types may be added in future
106/// versions without a breaking change.
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum DiagramType {
110    /// Flowchart / graph diagram (`flowchart` or `graph` keyword).
111    Flowchart,
112    /// Pie chart diagram (`pie` keyword).
113    Pie,
114    /// Sequence diagram (`sequenceDiagram` keyword).
115    Sequence,
116    /// Entity-relationship diagram (`erDiagram` keyword).
117    Er,
118    /// Gantt chart diagram (`gantt` keyword).
119    Gantt,
120    /// Info diagram (`info` keyword) — displays the Mermaid version string.
121    Info,
122    /// State diagram (`stateDiagram` or `stateDiagram-v2` keyword).
123    State,
124    /// Class diagram (`classDiagram` keyword).
125    Class,
126    /// Git graph diagram (`gitGraph` keyword).
127    Git,
128    /// Mind map diagram (`mindmap` keyword).
129    Mindmap,
130    /// Timeline diagram (`timeline` keyword).
131    Timeline,
132    /// Quadrant chart diagram (`quadrantChart` keyword).
133    Quadrant,
134    /// XY chart diagram (`xychart-beta` keyword).
135    XyChart,
136    /// C4 architecture diagram (`C4Context`, `C4Container`, `C4Component`,
137    /// `C4Dynamic`, or `C4Deployment` keyword).
138    C4,
139    /// Block diagram (`block-beta` keyword).
140    Block,
141    /// Packet diagram (`packet-beta` keyword).
142    Packet,
143    /// User journey diagram (`journey` keyword).
144    Journey,
145    /// Requirement diagram (`requirementDiagram` keyword).
146    Requirement,
147    /// Kanban board diagram (`kanban` keyword).
148    Kanban,
149    /// Sankey diagram (`sankey-beta` keyword, optionally preceded by YAML
150    /// front matter).
151    Sankey,
152    /// Treemap diagram (`treemap` or `treemap-beta` keyword).
153    Treemap,
154    /// Radar chart diagram (`radar` or `radar-beta` keyword).
155    Radar,
156    /// Venn diagram (`venn`, `venn-beta`, or `vennDiagram` keyword).
157    Venn,
158    /// Architecture diagram (`architecture` or `architecture-beta` keyword).
159    Architecture,
160    /// Event modeling diagram (`eventmodeling` or `event-modeling` keyword).
161    EventModeling,
162    /// Ishikawa / fishbone diagram (`ishikawa` or `fishbone` keyword).
163    Ishikawa,
164    /// Wardley map diagram (`wardley` keyword).
165    Wardley,
166    /// Tree-view diagram (`treeView-beta` or `treeview-beta` keyword).
167    TreeView,
168    /// The input did not match any recognised diagram keyword.
169    Unknown,
170}
171
172impl DiagramType {
173    /// Return the canonical string label used in error messages and
174    /// [`RenderError::diagram_type`](crate::error::RenderError::diagram_type).
175    fn label(&self) -> &'static str {
176        match self {
177            DiagramType::Flowchart => "flowchart",
178            DiagramType::Pie => "pie",
179            DiagramType::Sequence => "sequenceDiagram",
180            DiagramType::Er => "erDiagram",
181            DiagramType::Gantt => "gantt",
182            DiagramType::Info => "info",
183            DiagramType::State => "stateDiagram",
184            DiagramType::Class => "classDiagram",
185            DiagramType::Git => "gitGraph",
186            DiagramType::Mindmap => "mindmap",
187            DiagramType::Timeline => "timeline",
188            DiagramType::Quadrant => "quadrantChart",
189            DiagramType::XyChart => "xychart-beta",
190            DiagramType::C4 => "C4",
191            DiagramType::Block => "block-beta",
192            DiagramType::Packet => "packet-beta",
193            DiagramType::Journey => "journey",
194            DiagramType::Requirement => "requirementDiagram",
195            DiagramType::Kanban => "kanban",
196            DiagramType::Sankey => "sankey-beta",
197            DiagramType::Treemap => "treemap",
198            DiagramType::Radar => "radar",
199            DiagramType::Venn => "venn",
200            DiagramType::Architecture => "architecture",
201            DiagramType::EventModeling => "eventmodeling",
202            DiagramType::Ishikawa => "ishikawa",
203            DiagramType::Wardley => "wardley",
204            DiagramType::TreeView => "treeView-beta",
205            DiagramType::Unknown => "unknown",
206        }
207    }
208}
209
210/// Strip YAML front matter (--- ... ---) from input, returning the remainder.
211fn strip_frontmatter(input: &str) -> &str {
212    let trimmed = input.trim_start();
213    if !trimmed.starts_with("---") {
214        return input;
215    }
216    let after_open = &trimmed[3..];
217    if let Some(nl) = after_open.find('\n') {
218        let body_start = &after_open[nl + 1..];
219        if let Some(close_pos) = body_start.find("\n---") {
220            let remainder = &body_start[close_pos + 4..];
221            if let Some(nl2) = remainder.find('\n') {
222                return &remainder[nl2 + 1..];
223            }
224            return remainder;
225        }
226    }
227    input
228}
229
230/// Extract a human-readable message from a `catch_unwind` error payload.
231fn unwind_message(e: Box<dyn Any + Send>) -> String {
232    if let Some(s) = e.downcast_ref::<&str>() {
233        return s.to_string();
234    }
235    if let Some(s) = e.downcast_ref::<String>() {
236        return s.clone();
237    }
238    "Rendering failed".to_string()
239}
240
241/// Detect which [`DiagramType`] a Mermaid source string represents.
242///
243/// The function inspects the leading keyword(s) of `input` (after stripping
244/// ASCII whitespace) and returns the matching variant.  If no keyword matches,
245/// [`DiagramType::Unknown`] is returned.
246///
247/// For the Sankey diagram type the function also checks for an optional YAML
248/// front-matter block that precedes the `sankey-beta` keyword.
249///
250/// # Examples
251///
252/// ```
253/// use ariel_rs::{detect, DiagramType};
254///
255/// assert_eq!(detect("graph LR\n  A --> B"), DiagramType::Flowchart);
256/// assert_eq!(detect("pie\n  title Pets"), DiagramType::Pie);
257/// assert_eq!(detect("not a diagram"), DiagramType::Unknown);
258/// ```
259pub fn detect(input: &str) -> DiagramType {
260    // Strip YAML frontmatter before detection — any diagram type can have it.
261    let stripped = strip_frontmatter(input.trim_start());
262    let trimmed = stripped.trim_start();
263
264    if trimmed.starts_with("flowchart") || trimmed.starts_with("graph ") {
265        return DiagramType::Flowchart;
266    }
267    if trimmed.starts_with("pie") {
268        return DiagramType::Pie;
269    }
270    if trimmed.starts_with("sequenceDiagram") {
271        return DiagramType::Sequence;
272    }
273    if trimmed.starts_with("erDiagram") {
274        return DiagramType::Er;
275    }
276    if trimmed.starts_with("gantt") {
277        return DiagramType::Gantt;
278    }
279    if trimmed.starts_with("info") {
280        return DiagramType::Info;
281    }
282    if trimmed.starts_with("stateDiagram-v2") || trimmed.starts_with("stateDiagram") {
283        return DiagramType::State;
284    }
285    if trimmed.starts_with("classDiagram") {
286        return DiagramType::Class;
287    }
288    if trimmed.starts_with("gitGraph") {
289        return DiagramType::Git;
290    }
291    if trimmed.starts_with("mindmap") {
292        return DiagramType::Mindmap;
293    }
294    if trimmed.starts_with("timeline") {
295        return DiagramType::Timeline;
296    }
297    if trimmed.starts_with("quadrantChart") {
298        return DiagramType::Quadrant;
299    }
300    if trimmed.starts_with("xychart") {
301        return DiagramType::XyChart;
302    }
303    if trimmed.starts_with("C4Context")
304        || trimmed.starts_with("C4Container")
305        || trimmed.starts_with("C4Component")
306        || trimmed.starts_with("C4Dynamic")
307        || trimmed.starts_with("C4Deployment")
308    {
309        return DiagramType::C4;
310    }
311    if trimmed.starts_with("block") {
312        return DiagramType::Block;
313    }
314    if trimmed.starts_with("packet-beta") || trimmed.starts_with("packet") {
315        return DiagramType::Packet;
316    }
317    if trimmed.starts_with("journey") {
318        return DiagramType::Journey;
319    }
320    if trimmed.starts_with("requirementDiagram") || trimmed.starts_with("requirement") {
321        return DiagramType::Requirement;
322    }
323    if trimmed.starts_with("kanban") {
324        return DiagramType::Kanban;
325    }
326    if trimmed.starts_with("sankey")
327        || strip_frontmatter(trimmed)
328            .trim_start()
329            .starts_with("sankey")
330    {
331        return DiagramType::Sankey;
332    }
333    if trimmed.starts_with("treemap-beta") || trimmed.starts_with("treemap") {
334        return DiagramType::Treemap;
335    }
336    if trimmed.starts_with("radar-beta") || trimmed.starts_with("radar") {
337        return DiagramType::Radar;
338    }
339    if trimmed.starts_with("venn-beta")
340        || trimmed.starts_with("vennDiagram")
341        || trimmed.starts_with("venn")
342    {
343        return DiagramType::Venn;
344    }
345    if trimmed.starts_with("architecture-beta") || trimmed.starts_with("architecture") {
346        return DiagramType::Architecture;
347    }
348    if trimmed.starts_with("eventmodeling") || trimmed.starts_with("event-modeling") {
349        return DiagramType::EventModeling;
350    }
351    if trimmed.starts_with("fishbone") || trimmed.starts_with("ishikawa") {
352        return DiagramType::Ishikawa;
353    }
354    if trimmed.starts_with("wardley") {
355        return DiagramType::Wardley;
356    }
357    if trimmed.starts_with("treeView-beta")
358        || trimmed.starts_with("treeview-beta")
359        || trimmed.starts_with("treeView")
360    {
361        return DiagramType::TreeView;
362    }
363
364    DiagramType::Unknown
365}
366
367/// Render any Mermaid diagram source to an SVG string.
368///
369/// Returns an error SVG if the diagram type is unrecognized or if rendering
370/// panics for any reason.
371pub fn render(input: &str, theme: theme::Theme) -> String {
372    macro_rules! safe_render {
373        ($diagram_type:expr, $call:expr) => {{
374            let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
375            match result {
376                Ok(svg) => svg::normalize_floats(&svg),
377                Err(e) => {
378                    let msg = unwind_message(e);
379                    error_svg::render_error_svg($diagram_type, &msg)
380                }
381            }
382        }};
383    }
384
385    let dt = detect(input.trim_start());
386    let label = dt.label();
387    match dt {
388        DiagramType::Flowchart => {
389            safe_render!(label, diagrams::flowchart::render_html(input, theme))
390        }
391        DiagramType::Pie => safe_render!(label, diagrams::pie::render_html(input, theme)),
392        DiagramType::Sequence => safe_render!(label, diagrams::sequence::render_html(input, theme)),
393        DiagramType::Er => safe_render!(label, {
394            let d = diagrams::er::parser::parse(input);
395            diagrams::er::render(&d.diagram, theme)
396        }),
397        DiagramType::Gantt => safe_render!(label, diagrams::gantt::render_html(input, theme)),
398        DiagramType::Info => safe_render!(label, {
399            let d = diagrams::info::parser::parse(input);
400            diagrams::info::render(&d, theme)
401        }),
402        DiagramType::State => safe_render!(label, {
403            let d = diagrams::state::parser::parse(input);
404            diagrams::state::render(&d, theme, true)
405        }),
406        DiagramType::Class => {
407            safe_render!(label, diagrams::class_diagram::render_html(input, theme))
408        }
409        DiagramType::Git => safe_render!(label, diagrams::git::render_html(input, theme)),
410        DiagramType::Mindmap => safe_render!(label, diagrams::mindmap::render_html(input, theme)),
411        DiagramType::Timeline => safe_render!(label, diagrams::timeline::render_html(input, theme)),
412        DiagramType::Quadrant => safe_render!(label, diagrams::quadrant::render_html(input, theme)),
413        DiagramType::XyChart => safe_render!(label, diagrams::xychart::render_html(input, theme)),
414        DiagramType::C4 => safe_render!(label, diagrams::c4::render_html(input, theme)),
415        DiagramType::Block => safe_render!(label, diagrams::block::render_html(input, theme)),
416        DiagramType::Packet => safe_render!(label, diagrams::packet::render_html(input, theme)),
417        DiagramType::Journey => safe_render!(label, diagrams::journey::render_html(input, theme)),
418        DiagramType::Requirement => {
419            safe_render!(label, diagrams::requirement::render_html(input, theme))
420        }
421        DiagramType::Kanban => safe_render!(label, diagrams::kanban::render_html(input, theme)),
422        DiagramType::Sankey => safe_render!(label, diagrams::sankey::render_html(input, theme)),
423        DiagramType::Treemap => safe_render!(label, diagrams::treemap::render_html(input, theme)),
424        DiagramType::Radar => safe_render!(label, diagrams::radar::render_html(input, theme)),
425        DiagramType::Venn => safe_render!(label, diagrams::venn::render_html(input, theme)),
426        DiagramType::Architecture => {
427            safe_render!(label, diagrams::architecture::render_html(input, theme))
428        }
429        DiagramType::EventModeling => {
430            safe_render!(label, diagrams::eventmodeling::render_html(input, theme))
431        }
432        DiagramType::Ishikawa => safe_render!(label, diagrams::ishikawa::render_html(input, theme)),
433        DiagramType::Wardley => safe_render!(label, diagrams::wardley::render_html(input, theme)),
434        DiagramType::TreeView => {
435            safe_render!(label, diagrams::treeview::render_html(input, theme))
436        }
437        DiagramType::Unknown => error_svg::render_error_svg(label, "Unrecognized diagram type."),
438    }
439}
440
441/// Alias for [`render`] — renders any Mermaid diagram source to an SVG string.
442pub fn render_svg(input: &str, theme: theme::Theme) -> String {
443    render(input, theme)
444}
445
446/// Render any Mermaid diagram source to an SVG string.
447///
448/// Returns `Err(RenderError)` if the diagram type is unrecognised or if
449/// rendering panics for any reason. On success returns the SVG string.
450///
451/// For a version that never fails and returns an error SVG instead, use
452/// [`render`].
453///
454/// # Errors
455///
456/// Returns [`RenderError::unknown_type`] when the input does not match any
457/// supported diagram keyword, or [`RenderError::from_panic`] if the renderer
458/// panics internally.
459///
460/// # Examples
461///
462/// ```
463/// let result = ariel_rs::try_render("graph LR\n  A --> B", ariel_rs::theme::Theme::Default);
464/// assert!(result.is_ok());
465/// let svg = result.unwrap();
466/// assert!(svg.contains("<svg"));
467/// ```
468pub fn try_render(input: &str, theme: theme::Theme) -> Result<String, RenderError> {
469    macro_rules! safe_try_render {
470        ($diagram_type:expr, $call:expr) => {{
471            let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
472            match result {
473                Ok(svg) => Ok(svg::normalize_floats(&svg)),
474                Err(e) => {
475                    let msg = unwind_message(e);
476                    Err(RenderError::from_panic($diagram_type, msg))
477                }
478            }
479        }};
480    }
481
482    let dt = detect(input.trim_start());
483    let label = dt.label();
484    match dt {
485        DiagramType::Flowchart => {
486            safe_try_render!(label, diagrams::flowchart::render_html(input, theme))
487        }
488        DiagramType::Pie => safe_try_render!(label, diagrams::pie::render_html(input, theme)),
489        DiagramType::Sequence => {
490            safe_try_render!(label, diagrams::sequence::render_html(input, theme))
491        }
492        DiagramType::Er => safe_try_render!(label, {
493            let d = diagrams::er::parser::parse(input);
494            diagrams::er::render(&d.diagram, theme)
495        }),
496        DiagramType::Gantt => safe_try_render!(label, diagrams::gantt::render_html(input, theme)),
497        DiagramType::Info => safe_try_render!(label, {
498            let d = diagrams::info::parser::parse(input);
499            diagrams::info::render(&d, theme)
500        }),
501        DiagramType::State => safe_try_render!(label, {
502            let d = diagrams::state::parser::parse(input);
503            diagrams::state::render(&d, theme, true)
504        }),
505        DiagramType::Class => {
506            safe_try_render!(label, diagrams::class_diagram::render_html(input, theme))
507        }
508        DiagramType::Git => safe_try_render!(label, diagrams::git::render_html(input, theme)),
509        DiagramType::Mindmap => {
510            safe_try_render!(label, diagrams::mindmap::render_html(input, theme))
511        }
512        DiagramType::Timeline => {
513            safe_try_render!(label, diagrams::timeline::render_html(input, theme))
514        }
515        DiagramType::Quadrant => {
516            safe_try_render!(label, diagrams::quadrant::render_html(input, theme))
517        }
518        DiagramType::XyChart => {
519            safe_try_render!(label, diagrams::xychart::render_html(input, theme))
520        }
521        DiagramType::C4 => safe_try_render!(label, diagrams::c4::render_html(input, theme)),
522        DiagramType::Block => safe_try_render!(label, diagrams::block::render_html(input, theme)),
523        DiagramType::Packet => safe_try_render!(label, diagrams::packet::render_html(input, theme)),
524        DiagramType::Journey => {
525            safe_try_render!(label, diagrams::journey::render_html(input, theme))
526        }
527        DiagramType::Requirement => {
528            safe_try_render!(label, diagrams::requirement::render_html(input, theme))
529        }
530        DiagramType::Kanban => safe_try_render!(label, diagrams::kanban::render_html(input, theme)),
531        DiagramType::Sankey => safe_try_render!(label, diagrams::sankey::render_html(input, theme)),
532        DiagramType::Treemap => {
533            safe_try_render!(label, diagrams::treemap::render_html(input, theme))
534        }
535        DiagramType::Radar => safe_try_render!(label, diagrams::radar::render_html(input, theme)),
536        DiagramType::Venn => safe_try_render!(label, diagrams::venn::render_html(input, theme)),
537        DiagramType::Architecture => {
538            safe_try_render!(label, diagrams::architecture::render_html(input, theme))
539        }
540        DiagramType::EventModeling => {
541            safe_try_render!(label, diagrams::eventmodeling::render_html(input, theme))
542        }
543        DiagramType::Ishikawa => {
544            safe_try_render!(label, diagrams::ishikawa::render_html(input, theme))
545        }
546        DiagramType::Wardley => {
547            safe_try_render!(label, diagrams::wardley::render_html(input, theme))
548        }
549        DiagramType::TreeView => {
550            safe_try_render!(label, diagrams::treeview::render_html(input, theme))
551        }
552        DiagramType::Unknown => Err(RenderError::unknown_type()),
553    }
554}
555
556/// Render any Mermaid diagram source to an SVG string using [`RenderOptions`].
557///
558/// This is the options-aware counterpart of [`render`].  Currently only
559/// `options.theme` is forwarded to the underlying renderer; the remaining
560/// fields (`font_family`, `font_size`, `max_width`, `background`) are reserved
561/// for future use once individual renderers gain option support.
562///
563/// Returns an error SVG if the diagram type is unrecognized or if rendering
564/// panics for any reason.
565///
566/// # Examples
567///
568/// ```
569/// use ariel_rs::{render_with_options, RenderOptions, theme::Theme};
570///
571/// let opts = RenderOptions {
572///     theme: Theme::Forest,
573///     ..RenderOptions::default()
574/// };
575/// let svg = render_with_options("graph LR\n  A --> B", opts);
576/// assert!(svg.contains("<svg"));
577/// ```
578pub fn render_with_options(input: &str, options: RenderOptions) -> String {
579    render(input, options.theme)
580}
581
582/// Render any Mermaid diagram source to an SVG string using [`RenderOptions`].
583///
584/// This is the options-aware counterpart of [`try_render`].  Currently only
585/// `options.theme` is forwarded to the underlying renderer; the remaining
586/// fields (`font_family`, `font_size`, `max_width`, `background`) are reserved
587/// for future use once individual renderers gain option support.
588///
589/// Returns `Err(RenderError)` if the diagram type is unrecognised or if
590/// rendering panics for any reason.
591///
592/// # Errors
593///
594/// Returns [`RenderError::unknown_type`] when the input does not match any
595/// supported diagram keyword, or [`RenderError::from_panic`] if the renderer
596/// panics internally.
597///
598/// # Examples
599///
600/// ```
601/// use ariel_rs::{try_render_with_options, RenderOptions, theme::Theme};
602///
603/// let opts = RenderOptions {
604///     theme: Theme::Dark,
605///     max_width: Some(800.0),
606///     ..RenderOptions::default()
607/// };
608/// let result = try_render_with_options("pie\n  title Pets\n  \"Dogs\" : 40\n  \"Cats\" : 60", opts);
609/// assert!(result.is_ok());
610/// assert!(result.unwrap().contains("<svg"));
611/// ```
612pub fn try_render_with_options(input: &str, options: RenderOptions) -> Result<String, RenderError> {
613    try_render(input, options.theme)
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    // ── render() happy-path tests ────────────────────────────────────────────
621
622    #[test]
623    fn render_flowchart() {
624        let svg = render("flowchart TD\n  A --> B", theme::Theme::Default);
625        assert!(svg.contains("<svg"));
626        assert!(!svg.contains("Syntax error"));
627    }
628
629    #[test]
630    fn render_pie() {
631        let svg = render("pie title X\n  \"A\" : 1", theme::Theme::Default);
632        assert!(svg.contains("<svg"));
633    }
634
635    #[test]
636    fn render_sequence() {
637        let svg = render(
638            "sequenceDiagram\n  Alice->>Bob: Hello",
639            theme::Theme::Default,
640        );
641        assert!(svg.contains("<svg"));
642    }
643
644    #[test]
645    fn render_er() {
646        let svg = render("erDiagram\n  A ||--o{ B : has", theme::Theme::Default);
647        assert!(svg.contains("<svg"));
648    }
649
650    #[test]
651    fn render_gantt() {
652        let svg = render(
653            "gantt\n  dateFormat YYYY-MM-DD\n  section A\n  Task1: 2024-01-01, 7d",
654            theme::Theme::Default,
655        );
656        assert!(svg.contains("<svg"));
657    }
658
659    #[test]
660    fn render_state() {
661        let svg = render("stateDiagram-v2\n  [*] --> A", theme::Theme::Default);
662        assert!(svg.contains("<svg"));
663    }
664
665    #[test]
666    fn render_class() {
667        let svg = render("classDiagram\n  class A", theme::Theme::Default);
668        assert!(svg.contains("<svg"));
669    }
670
671    #[test]
672    fn render_git() {
673        let svg = render("gitGraph\n  commit", theme::Theme::Default);
674        assert!(svg.contains("<svg"));
675    }
676
677    // ── render() unknown type returns error SVG, no panic ────────────────────
678
679    #[test]
680    fn render_unknown_type_returns_error_svg() {
681        let svg = render("unknownDiagram\n  foo", theme::Theme::Default);
682        assert!(svg.contains("<svg"));
683    }
684
685    // ── try_render() ─────────────────────────────────────────────────────────
686
687    #[test]
688    fn try_render_ok() {
689        let result = try_render("pie title X\n  \"A\" : 1", theme::Theme::Default);
690        assert!(result.is_ok());
691        assert!(result.unwrap().contains("<svg"));
692    }
693
694    #[test]
695    fn try_render_unknown_returns_err() {
696        let result = try_render("unknownDiagram\n  foo", theme::Theme::Default);
697        assert!(result.is_err());
698    }
699
700    // ── detect() — every variant ──────────────────────────────────────────────
701
702    #[test]
703    fn detect_flowchart_keyword() {
704        assert_eq!(detect("flowchart TD\n  A --> B"), DiagramType::Flowchart);
705    }
706
707    #[test]
708    fn detect_graph_keyword() {
709        assert_eq!(detect("graph LR\n  A --> B"), DiagramType::Flowchart);
710    }
711
712    #[test]
713    fn detect_pie() {
714        assert_eq!(detect("pie title X\n  \"A\" : 1"), DiagramType::Pie);
715    }
716
717    #[test]
718    fn detect_sequence() {
719        assert_eq!(
720            detect("sequenceDiagram\n  A->>B: hi"),
721            DiagramType::Sequence
722        );
723    }
724
725    #[test]
726    fn detect_er() {
727        assert_eq!(detect("erDiagram\n  A ||--o{ B : has"), DiagramType::Er);
728    }
729
730    #[test]
731    fn detect_gantt() {
732        assert_eq!(detect("gantt\n  dateFormat YYYY-MM-DD"), DiagramType::Gantt);
733    }
734
735    #[test]
736    fn detect_state() {
737        assert_eq!(detect("stateDiagram\n  [*] --> A"), DiagramType::State);
738    }
739
740    #[test]
741    fn detect_state_v2() {
742        assert_eq!(detect("stateDiagram-v2\n  [*] --> A"), DiagramType::State);
743    }
744
745    #[test]
746    fn detect_class() {
747        assert_eq!(detect("classDiagram\n  class A"), DiagramType::Class);
748    }
749
750    #[test]
751    fn detect_git() {
752        assert_eq!(detect("gitGraph\n  commit"), DiagramType::Git);
753    }
754
755    #[test]
756    fn detect_mindmap() {
757        assert_eq!(detect("mindmap\n  root((A))"), DiagramType::Mindmap);
758    }
759
760    #[test]
761    fn detect_timeline() {
762        assert_eq!(detect("timeline\n  title History"), DiagramType::Timeline);
763    }
764
765    #[test]
766    fn detect_quadrant() {
767        assert_eq!(detect("quadrantChart\n  title Q"), DiagramType::Quadrant);
768    }
769
770    #[test]
771    fn detect_xychart() {
772        assert_eq!(detect("xychart-beta\n  line [1, 2]"), DiagramType::XyChart);
773    }
774
775    #[test]
776    fn detect_c4_context() {
777        assert_eq!(detect("C4Context\n  title T"), DiagramType::C4);
778    }
779
780    #[test]
781    fn detect_c4_container() {
782        assert_eq!(detect("C4Container\n  title T"), DiagramType::C4);
783    }
784
785    #[test]
786    fn detect_block() {
787        assert_eq!(detect("block-beta\n  A"), DiagramType::Block);
788    }
789
790    #[test]
791    fn detect_packet() {
792        assert_eq!(detect("packet-beta\n  0-7: A"), DiagramType::Packet);
793    }
794
795    #[test]
796    fn detect_journey() {
797        assert_eq!(detect("journey\n  title My"), DiagramType::Journey);
798    }
799
800    #[test]
801    fn detect_requirement() {
802        assert_eq!(
803            detect("requirementDiagram\n  requirement R {}"),
804            DiagramType::Requirement
805        );
806    }
807
808    #[test]
809    fn detect_kanban() {
810        assert_eq!(detect("kanban\n  Todo\n    task1"), DiagramType::Kanban);
811    }
812
813    #[test]
814    fn detect_sankey() {
815        assert_eq!(detect("sankey-beta\n  A,B,10"), DiagramType::Sankey);
816    }
817
818    #[test]
819    fn detect_treemap() {
820        assert_eq!(detect("treemap\n  root\n    A: 1"), DiagramType::Treemap);
821    }
822
823    #[test]
824    fn detect_treemap_beta() {
825        assert_eq!(
826            detect("treemap-beta\n  root\n    A: 1"),
827            DiagramType::Treemap
828        );
829    }
830
831    #[test]
832    fn detect_radar() {
833        assert_eq!(detect("radar\n  title R"), DiagramType::Radar);
834    }
835
836    #[test]
837    fn detect_radar_beta() {
838        assert_eq!(detect("radar-beta\n  title R"), DiagramType::Radar);
839    }
840
841    #[test]
842    fn detect_venn() {
843        assert_eq!(detect("venn\n  A"), DiagramType::Venn);
844    }
845
846    #[test]
847    fn detect_venn_beta() {
848        assert_eq!(detect("venn-beta\n  A"), DiagramType::Venn);
849    }
850
851    #[test]
852    fn detect_venn_diagram() {
853        assert_eq!(detect("vennDiagram\n  A"), DiagramType::Venn);
854    }
855
856    #[test]
857    fn detect_architecture() {
858        assert_eq!(
859            detect("architecture\n  service A"),
860            DiagramType::Architecture
861        );
862    }
863
864    #[test]
865    fn detect_architecture_beta() {
866        assert_eq!(
867            detect("architecture-beta\n  service A"),
868            DiagramType::Architecture
869        );
870    }
871
872    #[test]
873    fn detect_event_modeling() {
874        assert_eq!(detect("eventmodeling\n  A"), DiagramType::EventModeling);
875    }
876
877    #[test]
878    fn detect_event_modeling_hyphen() {
879        assert_eq!(detect("event-modeling\n  A"), DiagramType::EventModeling);
880    }
881
882    #[test]
883    fn detect_ishikawa() {
884        assert_eq!(detect("ishikawa\n  effect"), DiagramType::Ishikawa);
885    }
886
887    #[test]
888    fn detect_fishbone() {
889        assert_eq!(detect("fishbone\n  effect"), DiagramType::Ishikawa);
890    }
891
892    #[test]
893    fn detect_wardley() {
894        assert_eq!(detect("wardley\n  title W"), DiagramType::Wardley);
895    }
896
897    #[test]
898    fn detect_treeview_beta() {
899        assert_eq!(detect("treeView-beta\n    \"docs\""), DiagramType::TreeView);
900    }
901
902    #[test]
903    fn detect_treeview_lowercase() {
904        assert_eq!(detect("treeview-beta\n    \"docs\""), DiagramType::TreeView);
905    }
906
907    #[test]
908    fn render_treeview() {
909        let svg = render(
910            "treeView-beta\n    \"docs\"\n        \"build\"\n",
911            theme::Theme::Default,
912        );
913        assert!(svg.contains("<svg"));
914        assert!(svg.contains("tree-view"));
915        assert!(svg.contains("docs"));
916    }
917
918    #[test]
919    fn detect_unknown() {
920        assert_eq!(detect("not a diagram"), DiagramType::Unknown);
921    }
922
923    // ── render_with_options() ────────────────────────────────────────────────
924
925    #[test]
926    fn render_with_options_dark_theme() {
927        let opts = RenderOptions {
928            theme: theme::Theme::Dark,
929            ..Default::default()
930        };
931        let svg = render_with_options("pie title X\n  \"A\" : 1", opts);
932        assert!(svg.contains("<svg"));
933    }
934
935    #[test]
936    fn render_with_options_forest_theme() {
937        let opts = RenderOptions {
938            theme: theme::Theme::Forest,
939            ..Default::default()
940        };
941        let svg = render_with_options("flowchart TD\n  A --> B", opts);
942        assert!(svg.contains("<svg"));
943    }
944
945    // ── try_render_with_options() ────────────────────────────────────────────
946
947    #[test]
948    fn try_render_with_options_ok() {
949        let opts = RenderOptions {
950            theme: theme::Theme::Dark,
951            max_width: Some(800.0),
952            ..Default::default()
953        };
954        let result =
955            try_render_with_options("pie\n  title Pets\n  \"Dogs\" : 40\n  \"Cats\" : 60", opts);
956        assert!(result.is_ok());
957        assert!(result.unwrap().contains("<svg"));
958    }
959
960    #[test]
961    fn try_render_with_options_unknown_returns_err() {
962        let opts = RenderOptions::default();
963        let result = try_render_with_options("not a diagram", opts);
964        assert!(result.is_err());
965    }
966
967    // ── render_svg() alias ───────────────────────────────────────────────────
968
969    #[test]
970    fn render_svg_alias() {
971        let svg = render_svg(
972            "gantt\n  dateFormat YYYY-MM-DD\n  section A\n  Task1: 2024-01-01, 7d",
973            theme::Theme::Default,
974        );
975        assert!(svg.contains("<svg"));
976    }
977
978    // ── all themes produce SVG ───────────────────────────────────────────────
979
980    #[test]
981    fn all_themes_render() {
982        let input = "flowchart TD\n  A --> B";
983        for t in [
984            theme::Theme::Default,
985            theme::Theme::Dark,
986            theme::Theme::Forest,
987            theme::Theme::Neutral,
988        ] {
989            let svg = render(input, t);
990            assert!(svg.contains("<svg"));
991        }
992    }
993
994    // ── leading whitespace is ignored by detect ──────────────────────────────
995
996    #[test]
997    fn detect_ignores_leading_whitespace() {
998        assert_eq!(detect("   flowchart TD\n  A --> B"), DiagramType::Flowchart);
999    }
1000
1001    // ── sankey with YAML frontmatter ─────────────────────────────────────────
1002
1003    #[test]
1004    fn detect_sankey_with_frontmatter() {
1005        let input = "---\nconfig:\n  sankey:\n    showValues: false\n---\nsankey-beta\n  A,B,10";
1006        assert_eq!(detect(input), DiagramType::Sankey);
1007    }
1008}