use std::fmt;
use crate::js::chars::{is_space, is_word, space_run, unit};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagramType {
Flowchart,
Sequence,
Class,
State,
Er,
Pie,
Gantt,
GitGraph,
Mindmap,
Timeline,
Journey,
XyChart,
QuadrantChart,
Requirement,
C4,
Sankey,
Block,
Packet,
Kanban,
Architecture,
Radar,
Treemap,
Info,
AgentFlow,
UseCase,
Railroad,
TreeView,
Venn,
Cynefin,
Wardley,
Swimlane,
Ishikawa,
EventModeling,
}
impl DiagramType {
pub fn from_name(name: &str) -> Option<DiagramType> {
crate::types().iter().copied().find(|t| t.name() == name)
}
pub fn name(self) -> &'static str {
use DiagramType::*;
match self {
Flowchart => "flowchart",
Sequence => "sequence",
Class => "class",
State => "state",
Er => "er",
Pie => "pie",
Gantt => "gantt",
GitGraph => "gitGraph",
Mindmap => "mindmap",
Timeline => "timeline",
Journey => "journey",
XyChart => "xychart",
QuadrantChart => "quadrantChart",
Requirement => "requirement",
C4 => "c4",
Sankey => "sankey",
Block => "block",
Packet => "packet",
Kanban => "kanban",
Architecture => "architecture",
Radar => "radar",
Treemap => "treemap",
Info => "info",
AgentFlow => "agentflow",
UseCase => "usecase",
Railroad => "railroad",
TreeView => "treeView",
Venn => "venn",
Cynefin => "cynefin",
Wardley => "wardley",
Swimlane => "swimlane",
Ishikawa => "ishikawa",
EventModeling => "eventmodeling",
}
}
}
impl fmt::Display for DiagramType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Detected {
pub(crate) diagram: DiagramType,
pub(crate) mermaid_type: &'static str,
}
fn starts_with<'a>(s: &'a str, word: &str, fold: bool) -> Option<&'a str> {
let s = &s[space_run(s, 0)..];
let head = s.get(..word.len())?;
(head == word || fold && head.eq_ignore_ascii_case(word)).then(|| &s[word.len()..])
}
fn word_ends(rest: &str) -> bool {
rest.bytes().next().is_none_or(|c| !is_word(i32::from(c)))
}
enum Test {
Prefix(&'static str),
Then(&'static str, bool, fn(&str) -> bool),
Any(&'static [&'static str]),
}
impl Test {
fn matches(&self, s: &str) -> bool {
match *self {
Test::Prefix(word) => starts_with(s, word, false).is_some(),
Test::Then(word, fold, after) => starts_with(s, word, fold).is_some_and(after),
Test::Any(words) => words.iter().any(|w| starts_with(s, w, false).is_some()),
}
}
}
fn always(_: &str) -> bool {
true
}
use DiagramType as D;
const DETECTORS: &[(DiagramType, &str, Test)] = &[
(D::Flowchart, "flowchart-elk", Test::Prefix("flowchart-elk")),
(D::Mindmap, "mindmap", Test::Prefix("mindmap")),
(
D::Architecture,
"architecture",
Test::Prefix("architecture"),
),
(
D::AgentFlow,
"agentflow",
Test::Then("agentflow-beta", false, word_ends),
),
(
D::C4,
"c4",
Test::Any(&[
"C4Context",
"C4Container",
"C4Component",
"C4Dynamic",
"C4Deployment",
]),
),
(D::Kanban, "kanban", Test::Prefix("kanban")),
(D::Class, "classDiagram", Test::Prefix("classDiagram")),
(D::Er, "er", Test::Prefix("erDiagram")),
(D::Gantt, "gantt", Test::Prefix("gantt")),
(D::Info, "info", Test::Prefix("info")),
(D::Pie, "pie", Test::Prefix("pie")),
(D::Requirement, "requirement", Test::Prefix("requirement")),
(D::Sequence, "sequence", Test::Prefix("sequenceDiagram")),
(
D::Swimlane,
"swimlane",
Test::Then("swimlane-beta", false, word_ends),
),
(
D::Flowchart,
"flowchart-v2",
Test::Any(&["graph", "flowchart"]),
),
(D::Timeline, "timeline", Test::Prefix("timeline")),
(D::GitGraph, "gitGraph", Test::Prefix("gitGraph")),
(D::State, "stateDiagram", Test::Prefix("stateDiagram")),
(D::Journey, "journey", Test::Prefix("journey")),
(
D::QuadrantChart,
"quadrantChart",
Test::Prefix("quadrantChart"),
),
(D::Sankey, "sankey", Test::Prefix("sankey")),
(D::Packet, "packet", Test::Prefix("packet")),
(D::XyChart, "xychart", Test::Prefix("xychart")),
(D::Block, "block", Test::Prefix("block")),
(
D::EventModeling,
"eventmodeling",
Test::Prefix("eventmodeling"),
),
(D::TreeView, "treeView", Test::Prefix("treeView-beta")),
(D::Radar, "radar", Test::Prefix("radar-beta")),
(
D::Ishikawa,
"ishikawa",
Test::Then("ishikawa", true, word_ends),
),
(D::Treemap, "treemap", Test::Prefix("treemap")),
(
D::Railroad,
"railroad",
Test::Then("railroad-beta", true, always),
),
(
D::Railroad,
"railroadEbnf",
Test::Then("railroad-ebnf-beta", true, always),
),
(
D::Railroad,
"railroadAbnf",
Test::Then("railroad-abnf-beta", true, always),
),
(
D::Railroad,
"railroadPeg",
Test::Then("railroad-peg-beta", true, always),
),
(D::Venn, "venn", Test::Prefix("venn-beta")),
(
D::Wardley,
"wardley",
Test::Then("wardley-beta", true, always),
),
(
D::Cynefin,
"cynefin",
Test::Then("cynefin-beta", false, |rest| {
rest.is_empty() || rest.starts_with(':') || is_space(unit(rest, 0))
}),
),
(
D::UseCase,
"usecase",
Test::Then("usecase-beta", false, |rest| {
rest.is_empty() || is_space(unit(rest, 0))
}),
),
];
pub(crate) fn detect_diagram(cleaned: &str) -> Option<Detected> {
DETECTORS
.iter()
.find(|(_, _, test)| test.matches(cleaned))
.map(|&(diagram, mermaid_type, _)| Detected {
diagram,
mermaid_type,
})
}