#![deny(missing_docs)]
pub(crate) mod diagrams;
pub mod error;
pub(crate) mod error_svg;
pub(crate) mod style;
pub(crate) mod svg;
pub(crate) mod text;
pub(crate) mod text_browser_metrics;
pub mod theme;
pub use error::{ParseError, ParseResult, RenderError};
pub struct RenderOptions {
pub theme: theme::Theme,
pub font_family: Option<String>,
pub font_size: Option<f64>,
pub max_width: Option<f64>,
pub background: Option<String>,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
theme: theme::Theme::Default,
font_family: None,
font_size: None,
max_width: None,
background: None,
}
}
}
use std::any::Any;
use std::panic::{self, AssertUnwindSafe};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagramType {
Flowchart,
Pie,
Sequence,
Er,
Gantt,
State,
Class,
Git,
Mindmap,
Timeline,
Quadrant,
XyChart,
C4,
Block,
Packet,
Journey,
Requirement,
Kanban,
Sankey,
Treemap,
Radar,
Venn,
Architecture,
EventModeling,
Cynefin,
Ishikawa,
Wardley,
ZenUml,
Railroad,
Unknown,
}
impl DiagramType {
fn label(&self) -> &'static str {
match self {
DiagramType::Flowchart => "flowchart",
DiagramType::Pie => "pie",
DiagramType::Sequence => "sequenceDiagram",
DiagramType::Er => "erDiagram",
DiagramType::Gantt => "gantt",
DiagramType::State => "stateDiagram",
DiagramType::Class => "classDiagram",
DiagramType::Git => "gitGraph",
DiagramType::Mindmap => "mindmap",
DiagramType::Timeline => "timeline",
DiagramType::Quadrant => "quadrantChart",
DiagramType::XyChart => "xychart-beta",
DiagramType::C4 => "C4",
DiagramType::Block => "block-beta",
DiagramType::Packet => "packet-beta",
DiagramType::Journey => "journey",
DiagramType::Requirement => "requirementDiagram",
DiagramType::Kanban => "kanban",
DiagramType::Sankey => "sankey-beta",
DiagramType::Treemap => "treemap",
DiagramType::Radar => "radar",
DiagramType::Venn => "venn",
DiagramType::Architecture => "architecture",
DiagramType::EventModeling => "eventmodeling",
DiagramType::Cynefin => "cynefin",
DiagramType::Ishikawa => "ishikawa",
DiagramType::Wardley => "wardley",
DiagramType::ZenUml => "zenuml",
DiagramType::Railroad => "railroad",
DiagramType::Unknown => "unknown",
}
}
}
fn strip_frontmatter(input: &str) -> &str {
let trimmed = input.trim_start();
if !trimmed.starts_with("---") {
return input;
}
let after_open = &trimmed[3..];
if let Some(nl) = after_open.find('\n') {
let body_start = &after_open[nl + 1..];
if let Some(close_pos) = body_start.find("\n---") {
let remainder = &body_start[close_pos + 4..];
if let Some(nl2) = remainder.find('\n') {
return &remainder[nl2 + 1..];
}
return remainder;
}
}
input
}
fn unwind_message(e: Box<dyn Any + Send>) -> String {
if let Some(s) = e.downcast_ref::<&str>() {
return s.to_string();
}
if let Some(s) = e.downcast_ref::<String>() {
return s.clone();
}
"Rendering failed".to_string()
}
pub fn detect(input: &str) -> DiagramType {
let trimmed = input.trim_start();
if trimmed.starts_with("flowchart") || trimmed.starts_with("graph ") {
return DiagramType::Flowchart;
}
if trimmed.starts_with("pie") {
return DiagramType::Pie;
}
if trimmed.starts_with("sequenceDiagram") {
return DiagramType::Sequence;
}
if trimmed.starts_with("erDiagram") {
return DiagramType::Er;
}
if trimmed.starts_with("gantt") {
return DiagramType::Gantt;
}
if trimmed.starts_with("stateDiagram-v2") || trimmed.starts_with("stateDiagram") {
return DiagramType::State;
}
if trimmed.starts_with("classDiagram") {
return DiagramType::Class;
}
if trimmed.starts_with("gitGraph") {
return DiagramType::Git;
}
if trimmed.starts_with("mindmap") {
return DiagramType::Mindmap;
}
if trimmed.starts_with("timeline") {
return DiagramType::Timeline;
}
if trimmed.starts_with("quadrantChart") {
return DiagramType::Quadrant;
}
if trimmed.starts_with("xychart-beta") {
return DiagramType::XyChart;
}
if trimmed.starts_with("C4Context")
|| trimmed.starts_with("C4Container")
|| trimmed.starts_with("C4Component")
|| trimmed.starts_with("C4Dynamic")
|| trimmed.starts_with("C4Deployment")
{
return DiagramType::C4;
}
if trimmed.starts_with("block-beta") {
return DiagramType::Block;
}
if trimmed.starts_with("packet-beta") {
return DiagramType::Packet;
}
if trimmed.starts_with("journey") {
return DiagramType::Journey;
}
if trimmed.starts_with("requirementDiagram") {
return DiagramType::Requirement;
}
if trimmed.starts_with("kanban") {
return DiagramType::Kanban;
}
if trimmed.starts_with("sankey-beta")
|| strip_frontmatter(trimmed)
.trim_start()
.starts_with("sankey-beta")
{
return DiagramType::Sankey;
}
if trimmed.starts_with("treemap-beta") || trimmed.starts_with("treemap") {
return DiagramType::Treemap;
}
if trimmed.starts_with("radar-beta") || trimmed.starts_with("radar") {
return DiagramType::Radar;
}
if trimmed.starts_with("venn-beta")
|| trimmed.starts_with("vennDiagram")
|| trimmed.starts_with("venn")
{
return DiagramType::Venn;
}
if trimmed.starts_with("architecture-beta") || trimmed.starts_with("architecture") {
return DiagramType::Architecture;
}
if trimmed.starts_with("eventmodeling") || trimmed.starts_with("event-modeling") {
return DiagramType::EventModeling;
}
if trimmed.starts_with("cynefin") {
return DiagramType::Cynefin;
}
if trimmed.starts_with("fishbone") || trimmed.starts_with("ishikawa") {
return DiagramType::Ishikawa;
}
if trimmed.starts_with("wardley") {
return DiagramType::Wardley;
}
if trimmed.starts_with("zenuml") {
return DiagramType::ZenUml;
}
if trimmed.starts_with("railroad-beta") || trimmed.starts_with("railroad") {
return DiagramType::Railroad;
}
DiagramType::Unknown
}
pub fn render(input: &str, theme: theme::Theme) -> String {
macro_rules! safe_render {
($diagram_type:expr, $call:expr) => {{
let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
match result {
Ok(svg) => svg,
Err(e) => {
let msg = unwind_message(e);
error_svg::render_error_svg($diagram_type, &msg)
}
}
}};
}
let dt = detect(input.trim_start());
let label = dt.label();
match dt {
DiagramType::Flowchart => {
safe_render!(label, diagrams::flowchart::render_html(input, theme))
}
DiagramType::Pie => safe_render!(label, diagrams::pie::render_html(input, theme)),
DiagramType::Sequence => safe_render!(label, diagrams::sequence::render_html(input, theme)),
DiagramType::Er => safe_render!(label, diagrams::er::render_html(input, theme)),
DiagramType::Gantt => safe_render!(label, diagrams::gantt::render_html(input, theme)),
DiagramType::State => safe_render!(label, diagrams::state::render_html(input, theme)),
DiagramType::Class => {
safe_render!(label, diagrams::class_diagram::render_html(input, theme))
}
DiagramType::Git => safe_render!(label, diagrams::git::render_html(input, theme)),
DiagramType::Mindmap => safe_render!(label, diagrams::mindmap::render_html(input, theme)),
DiagramType::Timeline => safe_render!(label, diagrams::timeline::render_html(input, theme)),
DiagramType::Quadrant => safe_render!(label, diagrams::quadrant::render_html(input, theme)),
DiagramType::XyChart => safe_render!(label, diagrams::xychart::render_html(input, theme)),
DiagramType::C4 => safe_render!(label, diagrams::c4::render_html(input, theme)),
DiagramType::Block => safe_render!(label, diagrams::block::render_html(input, theme)),
DiagramType::Packet => safe_render!(label, diagrams::packet::render_html(input, theme)),
DiagramType::Journey => safe_render!(label, diagrams::journey::render_html(input, theme)),
DiagramType::Requirement => {
safe_render!(label, diagrams::requirement::render_html(input, theme))
}
DiagramType::Kanban => safe_render!(label, diagrams::kanban::render_html(input, theme)),
DiagramType::Sankey => safe_render!(label, diagrams::sankey::render_html(input, theme)),
DiagramType::Treemap => safe_render!(label, diagrams::treemap::render_html(input, theme)),
DiagramType::Radar => safe_render!(label, diagrams::radar::render_html(input, theme)),
DiagramType::Venn => safe_render!(label, diagrams::venn::render_html(input, theme)),
DiagramType::Architecture => {
safe_render!(label, diagrams::architecture::render_html(input, theme))
}
DiagramType::EventModeling => {
safe_render!(label, diagrams::eventmodeling::render_html(input, theme))
}
DiagramType::Cynefin => safe_render!(label, diagrams::cynefin::render_html(input, theme)),
DiagramType::Ishikawa => safe_render!(label, diagrams::ishikawa::render_html(input, theme)),
DiagramType::Wardley => safe_render!(label, diagrams::wardley::render_html(input, theme)),
DiagramType::ZenUml => safe_render!(label, diagrams::zenuml::render_html(input, theme)),
DiagramType::Railroad => safe_render!(label, diagrams::railroad::render_html(input, theme)),
DiagramType::Unknown => error_svg::render_error_svg(label, "Unrecognized diagram type."),
}
}
pub fn render_svg(input: &str, theme: theme::Theme) -> String {
render(input, theme)
}
pub fn try_render(input: &str, theme: theme::Theme) -> Result<String, RenderError> {
macro_rules! safe_try_render {
($diagram_type:expr, $call:expr) => {{
let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
match result {
Ok(svg) => Ok(svg),
Err(e) => {
let msg = unwind_message(e);
Err(RenderError::from_panic($diagram_type, msg))
}
}
}};
}
let dt = detect(input.trim_start());
let label = dt.label();
match dt {
DiagramType::Flowchart => {
safe_try_render!(label, diagrams::flowchart::render_html(input, theme))
}
DiagramType::Pie => safe_try_render!(label, diagrams::pie::render_html(input, theme)),
DiagramType::Sequence => {
safe_try_render!(label, diagrams::sequence::render_html(input, theme))
}
DiagramType::Er => safe_try_render!(label, diagrams::er::render_html(input, theme)),
DiagramType::Gantt => safe_try_render!(label, diagrams::gantt::render_html(input, theme)),
DiagramType::State => safe_try_render!(label, diagrams::state::render_html(input, theme)),
DiagramType::Class => {
safe_try_render!(label, diagrams::class_diagram::render_html(input, theme))
}
DiagramType::Git => safe_try_render!(label, diagrams::git::render_html(input, theme)),
DiagramType::Mindmap => {
safe_try_render!(label, diagrams::mindmap::render_html(input, theme))
}
DiagramType::Timeline => {
safe_try_render!(label, diagrams::timeline::render_html(input, theme))
}
DiagramType::Quadrant => {
safe_try_render!(label, diagrams::quadrant::render_html(input, theme))
}
DiagramType::XyChart => {
safe_try_render!(label, diagrams::xychart::render_html(input, theme))
}
DiagramType::C4 => safe_try_render!(label, diagrams::c4::render_html(input, theme)),
DiagramType::Block => safe_try_render!(label, diagrams::block::render_html(input, theme)),
DiagramType::Packet => safe_try_render!(label, diagrams::packet::render_html(input, theme)),
DiagramType::Journey => {
safe_try_render!(label, diagrams::journey::render_html(input, theme))
}
DiagramType::Requirement => {
safe_try_render!(label, diagrams::requirement::render_html(input, theme))
}
DiagramType::Kanban => safe_try_render!(label, diagrams::kanban::render_html(input, theme)),
DiagramType::Sankey => safe_try_render!(label, diagrams::sankey::render_html(input, theme)),
DiagramType::Treemap => {
safe_try_render!(label, diagrams::treemap::render_html(input, theme))
}
DiagramType::Radar => safe_try_render!(label, diagrams::radar::render_html(input, theme)),
DiagramType::Venn => safe_try_render!(label, diagrams::venn::render_html(input, theme)),
DiagramType::Architecture => {
safe_try_render!(label, diagrams::architecture::render_html(input, theme))
}
DiagramType::EventModeling => {
safe_try_render!(label, diagrams::eventmodeling::render_html(input, theme))
}
DiagramType::Cynefin => {
safe_try_render!(label, diagrams::cynefin::render_html(input, theme))
}
DiagramType::Ishikawa => {
safe_try_render!(label, diagrams::ishikawa::render_html(input, theme))
}
DiagramType::Wardley => {
safe_try_render!(label, diagrams::wardley::render_html(input, theme))
}
DiagramType::ZenUml => safe_try_render!(label, diagrams::zenuml::render_html(input, theme)),
DiagramType::Railroad => {
safe_try_render!(label, diagrams::railroad::render_html(input, theme))
}
DiagramType::Unknown => Err(RenderError::unknown_type()),
}
}
pub fn render_with_options(input: &str, options: RenderOptions) -> String {
render(input, options.theme)
}
pub fn try_render_with_options(input: &str, options: RenderOptions) -> Result<String, RenderError> {
try_render(input, options.theme)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_flowchart() {
let svg = render("flowchart TD\n A --> B", theme::Theme::Default);
assert!(svg.contains("<svg"));
assert!(!svg.contains("Syntax error"));
}
#[test]
fn render_pie() {
let svg = render("pie title X\n \"A\" : 1", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_sequence() {
let svg = render(
"sequenceDiagram\n Alice->>Bob: Hello",
theme::Theme::Default,
);
assert!(svg.contains("<svg"));
}
#[test]
fn render_er() {
let svg = render("erDiagram\n A ||--o{ B : has", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_gantt() {
let svg = render(
"gantt\n dateFormat YYYY-MM-DD\n section A\n Task1: 2024-01-01, 7d",
theme::Theme::Default,
);
assert!(svg.contains("<svg"));
}
#[test]
fn render_state() {
let svg = render("stateDiagram-v2\n [*] --> A", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_class() {
let svg = render("classDiagram\n class A", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_git() {
let svg = render("gitGraph\n commit", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_zenuml() {
let svg = render("zenuml\n Alice->Bob: Hi", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn render_unknown_type_returns_error_svg() {
let svg = render("unknownDiagram\n foo", theme::Theme::Default);
assert!(svg.contains("<svg"));
}
#[test]
fn try_render_ok() {
let result = try_render("pie title X\n \"A\" : 1", theme::Theme::Default);
assert!(result.is_ok());
assert!(result.unwrap().contains("<svg"));
}
#[test]
fn try_render_unknown_returns_err() {
let result = try_render("unknownDiagram\n foo", theme::Theme::Default);
assert!(result.is_err());
}
#[test]
fn detect_flowchart_keyword() {
assert_eq!(detect("flowchart TD\n A --> B"), DiagramType::Flowchart);
}
#[test]
fn detect_graph_keyword() {
assert_eq!(detect("graph LR\n A --> B"), DiagramType::Flowchart);
}
#[test]
fn detect_pie() {
assert_eq!(detect("pie title X\n \"A\" : 1"), DiagramType::Pie);
}
#[test]
fn detect_sequence() {
assert_eq!(
detect("sequenceDiagram\n A->>B: hi"),
DiagramType::Sequence
);
}
#[test]
fn detect_er() {
assert_eq!(detect("erDiagram\n A ||--o{ B : has"), DiagramType::Er);
}
#[test]
fn detect_gantt() {
assert_eq!(detect("gantt\n dateFormat YYYY-MM-DD"), DiagramType::Gantt);
}
#[test]
fn detect_state() {
assert_eq!(detect("stateDiagram\n [*] --> A"), DiagramType::State);
}
#[test]
fn detect_state_v2() {
assert_eq!(detect("stateDiagram-v2\n [*] --> A"), DiagramType::State);
}
#[test]
fn detect_class() {
assert_eq!(detect("classDiagram\n class A"), DiagramType::Class);
}
#[test]
fn detect_git() {
assert_eq!(detect("gitGraph\n commit"), DiagramType::Git);
}
#[test]
fn detect_mindmap() {
assert_eq!(detect("mindmap\n root((A))"), DiagramType::Mindmap);
}
#[test]
fn detect_timeline() {
assert_eq!(detect("timeline\n title History"), DiagramType::Timeline);
}
#[test]
fn detect_quadrant() {
assert_eq!(detect("quadrantChart\n title Q"), DiagramType::Quadrant);
}
#[test]
fn detect_xychart() {
assert_eq!(detect("xychart-beta\n line [1, 2]"), DiagramType::XyChart);
}
#[test]
fn detect_c4_context() {
assert_eq!(detect("C4Context\n title T"), DiagramType::C4);
}
#[test]
fn detect_c4_container() {
assert_eq!(detect("C4Container\n title T"), DiagramType::C4);
}
#[test]
fn detect_block() {
assert_eq!(detect("block-beta\n A"), DiagramType::Block);
}
#[test]
fn detect_packet() {
assert_eq!(detect("packet-beta\n 0-7: A"), DiagramType::Packet);
}
#[test]
fn detect_journey() {
assert_eq!(detect("journey\n title My"), DiagramType::Journey);
}
#[test]
fn detect_requirement() {
assert_eq!(
detect("requirementDiagram\n requirement R {}"),
DiagramType::Requirement
);
}
#[test]
fn detect_kanban() {
assert_eq!(detect("kanban\n Todo\n task1"), DiagramType::Kanban);
}
#[test]
fn detect_sankey() {
assert_eq!(detect("sankey-beta\n A,B,10"), DiagramType::Sankey);
}
#[test]
fn detect_treemap() {
assert_eq!(detect("treemap\n root\n A: 1"), DiagramType::Treemap);
}
#[test]
fn detect_treemap_beta() {
assert_eq!(
detect("treemap-beta\n root\n A: 1"),
DiagramType::Treemap
);
}
#[test]
fn detect_radar() {
assert_eq!(detect("radar\n title R"), DiagramType::Radar);
}
#[test]
fn detect_radar_beta() {
assert_eq!(detect("radar-beta\n title R"), DiagramType::Radar);
}
#[test]
fn detect_venn() {
assert_eq!(detect("venn\n A"), DiagramType::Venn);
}
#[test]
fn detect_venn_beta() {
assert_eq!(detect("venn-beta\n A"), DiagramType::Venn);
}
#[test]
fn detect_venn_diagram() {
assert_eq!(detect("vennDiagram\n A"), DiagramType::Venn);
}
#[test]
fn detect_architecture() {
assert_eq!(
detect("architecture\n service A"),
DiagramType::Architecture
);
}
#[test]
fn detect_architecture_beta() {
assert_eq!(
detect("architecture-beta\n service A"),
DiagramType::Architecture
);
}
#[test]
fn detect_event_modeling() {
assert_eq!(detect("eventmodeling\n A"), DiagramType::EventModeling);
}
#[test]
fn detect_event_modeling_hyphen() {
assert_eq!(detect("event-modeling\n A"), DiagramType::EventModeling);
}
#[test]
fn detect_cynefin() {
assert_eq!(detect("cynefin\n A"), DiagramType::Cynefin);
}
#[test]
fn detect_ishikawa() {
assert_eq!(detect("ishikawa\n effect"), DiagramType::Ishikawa);
}
#[test]
fn detect_fishbone() {
assert_eq!(detect("fishbone\n effect"), DiagramType::Ishikawa);
}
#[test]
fn detect_wardley() {
assert_eq!(detect("wardley\n title W"), DiagramType::Wardley);
}
#[test]
fn detect_zenuml() {
assert_eq!(detect("zenuml\n A->B: hi"), DiagramType::ZenUml);
}
#[test]
fn detect_railroad() {
assert_eq!(detect("railroad\n A"), DiagramType::Railroad);
}
#[test]
fn detect_railroad_beta() {
assert_eq!(detect("railroad-beta\n A"), DiagramType::Railroad);
}
#[test]
fn detect_unknown() {
assert_eq!(detect("not a diagram"), DiagramType::Unknown);
}
#[test]
fn render_with_options_dark_theme() {
let opts = RenderOptions {
theme: theme::Theme::Dark,
..Default::default()
};
let svg = render_with_options("pie title X\n \"A\" : 1", opts);
assert!(svg.contains("<svg"));
}
#[test]
fn render_with_options_forest_theme() {
let opts = RenderOptions {
theme: theme::Theme::Forest,
..Default::default()
};
let svg = render_with_options("flowchart TD\n A --> B", opts);
assert!(svg.contains("<svg"));
}
#[test]
fn try_render_with_options_ok() {
let opts = RenderOptions {
theme: theme::Theme::Dark,
max_width: Some(800.0),
..Default::default()
};
let result =
try_render_with_options("pie\n title Pets\n \"Dogs\" : 40\n \"Cats\" : 60", opts);
assert!(result.is_ok());
assert!(result.unwrap().contains("<svg"));
}
#[test]
fn try_render_with_options_unknown_returns_err() {
let opts = RenderOptions::default();
let result = try_render_with_options("not a diagram", opts);
assert!(result.is_err());
}
#[test]
fn render_svg_alias() {
let svg = render_svg(
"gantt\n dateFormat YYYY-MM-DD\n section A\n Task1: 2024-01-01, 7d",
theme::Theme::Default,
);
assert!(svg.contains("<svg"));
}
#[test]
fn all_themes_render() {
let input = "flowchart TD\n A --> B";
for t in [
theme::Theme::Default,
theme::Theme::Dark,
theme::Theme::Forest,
theme::Theme::Neutral,
] {
let svg = render(input, t);
assert!(svg.contains("<svg"));
}
}
#[test]
fn detect_ignores_leading_whitespace() {
assert_eq!(detect(" flowchart TD\n A --> B"), DiagramType::Flowchart);
}
#[test]
fn detect_sankey_with_frontmatter() {
let input = "---\nconfig:\n sankey:\n showValues: false\n---\nsankey-beta\n A,B,10";
assert_eq!(detect(input), DiagramType::Sankey);
}
}