///|
fn first_non_empty_line(lines : Array[String]) -> (Int, String) {
for i, line in lines {
let t = trim_line(line)
if t.length() > 0 {
return (i, t)
}
}
(0, "")
}
///|
fn parse_direction_from_header(header : String) -> Direction {
let parts = split_ws(header)
if parts.length() >= 2 {
match parts[1].to_upper() {
"TD" => Direction::TD
"TB" => Direction::TD
"LR" => Direction::LR
"RL" => Direction::RL
"BT" => Direction::BT
_ => Direction::TD
}
} else {
Direction::TD
}
}
///|
fn parse_node_token(token : String, default_shape : NodeShape) -> GraphNode {
let t = trim_line(token)
match split_once(t, "[") {
Some((id_raw, rest)) =>
match split_once(rest, "]") {
Some((label_raw, _)) => {
let id = trim_line(id_raw)
let label = strip_quotes(trim_line(label_raw))
let sections = [[label]]
{ id, label, shape: NodeShape::Rect, sections }
}
None => {
let id = trim_line(id_raw)
let sections = [[id]]
{ id, label: id, shape: NodeShape::Rect, sections }
}
}
None =>
match split_once(t, "((") {
Some((id_raw, rest)) =>
match split_once(rest, "))") {
Some((label_raw, _)) => {
let id = trim_line(id_raw)
let label = trim_line(label_raw)
let sections = [[label]]
{ id, label, shape: NodeShape::Circle, sections }
}
None => {
let id = trim_line(id_raw)
let sections = [[id]]
{ id, label: id, shape: NodeShape::Circle, sections }
}
}
None =>
match split_once(t, "(") {
Some((id_raw, rest)) =>
match split_once(rest, ")") {
Some((label_raw, _)) => {
let id = trim_line(id_raw)
let label = trim_line(label_raw)
let sections = [[label]]
{ id, label, shape: NodeShape::Rounded, sections }
}
None => {
let id = trim_line(id_raw)
let sections = [[id]]
{ id, label: id, shape: NodeShape::Rounded, sections }
}
}
None => {
let id = trim_line(t)
let sections = [[id]]
{ id, label: id, shape: default_shape, sections }
}
}
}
}
}
///|
fn upsert_node(map : Map[String, GraphNode], node : GraphNode) -> Unit {
match map.get(node.id) {
None => map.set(node.id, node)
Some(existing) => {
let mut shape = existing.shape
if existing.shape == NodeShape::Rect && node.shape != NodeShape::Rect {
shape = node.shape
}
let mut label = existing.label
if existing.label == existing.id && node.label != existing.id {
label = node.label
}
let sections = if node.sections.length() > 0 {
node.sections
} else {
existing.sections
}
map.set(node.id, { id: existing.id, label, shape, sections })
}
}
}
///|
fn parse_edge_label_and_target(raw : String) -> (String, String?) {
let right = trim_line(raw)
if right.has_prefix("|") {
let rest = drop_first_char(right)
match split_once(rest, "|") {
Some((label_raw, target_raw)) => {
let label = trim_line(label_raw)
let target = trim_line(target_raw)
(target, Some(label))
}
None => (right, None)
}
} else {
(right, None)
}
}
///|
fn parse_edge_chain(
line : String,
nodes : Map[String, GraphNode],
edges : Array[GraphEdge],
default_shape : NodeShape,
) -> Unit {
let mut remaining = trim_line(line)
let mut prev_node : GraphNode? = None
let mut prev_label : String? = None
while true {
let arrow = if remaining.contains("-->") {
"-->"
} else if remaining.contains("->") {
"->"
} else if remaining.contains("--") {
"--"
} else {
""
}
if arrow.length() == 0 {
let node = parse_node_token(remaining, default_shape)
upsert_node(nodes, node)
match prev_node {
Some(prev) =>
edges.push({ from: prev.id, to: node.id, label: prev_label })
None => ()
}
break
}
match split_once(remaining, arrow) {
Some((left_raw, right_raw)) => {
let left = match split_once(left_raw, "--") {
Some((fst, _)) => fst
None => left_raw
}
let node = parse_node_token(left, default_shape)
upsert_node(nodes, node)
match prev_node {
Some(prev) =>
edges.push({ from: prev.id, to: node.id, label: prev_label })
None => ()
}
let (right_token, label) = parse_edge_label_and_target(right_raw)
prev_node = Some(node)
prev_label = label
remaining = right_token
}
None => break
}
}
}
///|
fn parse_flow_edges(
lines : Array[String],
header_index : Int,
default_shape : NodeShape,
) -> (Array[GraphNode], Array[GraphEdge]) {
let nodes : Map[String, GraphNode] = Map::new()
let edges : Array[GraphEdge] = []
for i, line in lines {
if i <= header_index {
continue
}
let t = trim_line(line)
if t.length() == 0 || t.has_prefix("%%") {
continue
}
parse_edge_chain(t, nodes, edges, default_shape)
}
let nodes_array = Array::from_iter(nodes.values())
(nodes_array, edges)
}
///|
fn parse_state(
lines : Array[String],
header_index : Int,
direction : Direction,
) -> GraphDiagram {
let nodes : Map[String, GraphNode] = Map::new()
let edges : Array[GraphEdge] = []
let mut start_counter = 0
let mut end_counter = 0
for i, line in lines {
if i <= header_index {
continue
}
let t = trim_line(line)
if t.length() == 0 || t.has_prefix("%%") {
continue
}
if t == "}" {
continue
}
match split_once(t, "state ") {
Some((prefix, rest)) => {
if prefix.trim().length() == 0 {
if rest.contains("{") {
match split_once(rest, "{") {
Some((name_raw, _)) => {
let name = trim_line(name_raw)
let node = parse_node_token(name, NodeShape::Rounded)
upsert_node(nodes, node)
()
}
None => ()
}
} else if rest.contains(" as ") {
match split_once(rest, " as ") {
Some((label_raw, id_raw)) => {
let label = strip_quotes(label_raw)
let id = trim_line(id_raw)
let sections = [[label]]
upsert_node(nodes, {
id,
label,
shape: NodeShape::Rounded,
sections,
})
()
}
None => ()
}
}
continue
}
()
}
None => ()
}
match split_once(t, " : ") {
Some((id_raw, label_raw)) => {
let id = trim_line(id_raw)
let label = trim_line(label_raw)
let sections = [[label]]
upsert_node(nodes, { id, label, shape: NodeShape::Rounded, sections })
continue
}
None => ()
}
let arrow = if t.contains("-->") {
"-->"
} else if t.contains("->") {
"->"
} else {
""
}
if arrow.length() > 0 {
match split_once(t, arrow) {
Some((left_raw, right_raw)) => {
let left_token = trim_line(left_raw)
let (right_token, label) = parse_edge_label_and_target(right_raw)
let left_node = if left_token == "[*]" {
start_counter += 1
let id = "_state_start_\{start_counter}"
{ id, label: "", shape: NodeShape::StateStart, sections: [[""]] }
} else {
parse_node_token(left_token, NodeShape::Rounded)
}
let right_node = if right_token == "[*]" {
end_counter += 1
let id = "_state_end_\{end_counter}"
{ id, label: "", shape: NodeShape::StateEnd, sections: [[""]] }
} else {
parse_node_token(right_token, NodeShape::Rounded)
}
upsert_node(nodes, left_node)
upsert_node(nodes, right_node)
edges.push({ from: left_node.id, to: right_node.id, label })
()
}
None => ()
}
}
}
let nodes_array = Array::from_iter(nodes.values())
{ kind: DiagramKind::State, direction, nodes: nodes_array, edges }
}
///|
fn parse_flowchart(
lines : Array[String],
header_index : Int,
direction : Direction,
) -> GraphDiagram {
let (nodes, edges) = parse_flow_edges(lines, header_index, NodeShape::Rect)
{ kind: DiagramKind::Flowchart, direction, nodes, edges }
}
///|
fn parse_class(lines : Array[String], header_index : Int) -> GraphDiagram {
let nodes : Map[String, GraphNode] = Map::new()
let edges : Array[GraphEdge] = []
let mut current_class : String? = None
for i, line in lines {
if i <= header_index {
continue
}
let t = trim_line(line)
if t.length() == 0 || t.has_prefix("%%") {
continue
}
if t == "}" {
current_class = None
continue
}
if t.has_prefix("class ") {
let parts = split_ws(t)
if parts.length() >= 2 {
let id = parts[1]
let sections = [[id], []]
upsert_node(nodes, { id, label: id, shape: NodeShape::Rect, sections })
if t.contains("{") {
current_class = Some(id)
}
}
continue
}
match current_class {
Some(id) => {
let member_line = t
let node = nodes
.get(id)
.unwrap_or({
id,
label: id,
shape: NodeShape::Rect,
sections: [[id], []],
})
let sections = node.sections
if sections.length() == 1 {
sections.push([])
}
sections[1].push(member_line)
nodes.set(id, { id, label: node.label, shape: node.shape, sections })
continue
}
None => ()
}
match split_once(t, ":") {
Some((left_raw, right_raw)) => {
let id = trim_line(left_raw)
let member_line = trim_line(right_raw)
let node = nodes
.get(id)
.unwrap_or({
id,
label: id,
shape: NodeShape::Rect,
sections: [[id], []],
})
let sections = node.sections
if sections.length() == 1 {
sections.push([])
}
sections[1].push(member_line)
nodes.set(id, { id, label: node.label, shape: node.shape, sections })
continue
}
None => ()
}
let parts = split_ws(t)
if parts.length() >= 3 && parts[1].contains("--") {
let from = parts[0]
let to = parts[2]
let left = parse_node_token(from, NodeShape::Rect)
let right = parse_node_token(to, NodeShape::Rect)
upsert_node(nodes, left)
upsert_node(nodes, right)
edges.push({ from, to, label: None })
continue
}
}
let nodes_array = Array::from_iter(nodes.values())
{
kind: DiagramKind::Class,
direction: Direction::TD,
nodes: nodes_array,
edges,
}
}
///|
fn parse_er(lines : Array[String], header_index : Int) -> GraphDiagram {
let nodes : Map[String, GraphNode] = Map::new()
let edges : Array[GraphEdge] = []
let mut current_entity : String? = None
for i, line in lines {
if i <= header_index {
continue
}
let t = trim_line(line)
if t.length() == 0 || t.has_prefix("%%") {
continue
}
if t == "}" {
current_entity = None
continue
}
if t.contains("--") {
let (left, label_opt) = match split_once(t, ":") {
Some((l, r)) => (l, Some(trim_line(r)))
None => (t, None)
}
let parts = split_ws(left)
if parts.length() >= 3 {
let from = parts[0]
let to = parts[2]
let left_node = parse_node_token(from, NodeShape::Rect)
let right_node = parse_node_token(to, NodeShape::Rect)
upsert_node(nodes, left_node)
upsert_node(nodes, right_node)
edges.push({ from, to, label: label_opt })
}
continue
}
if t.has_suffix("{") {
let parts = split_ws(t)
if parts.length() >= 1 {
let id = parts[0]
let sections = [[id], []]
upsert_node(nodes, { id, label: id, shape: NodeShape::Rect, sections })
current_entity = Some(id)
}
continue
}
match current_entity {
Some(id) => {
let attr_line = t
let node = nodes
.get(id)
.unwrap_or({
id,
label: id,
shape: NodeShape::Rect,
sections: [[id], []],
})
let sections = node.sections
if sections.length() == 1 {
sections.push([])
}
sections[1].push(attr_line)
nodes.set(id, { id, label: node.label, shape: node.shape, sections })
continue
}
None => ()
}
()
}
let nodes_array = Array::from_iter(nodes.values())
{ kind: DiagramKind::Er, direction: Direction::TD, nodes: nodes_array, edges }
}
///|
fn ensure_actor(
map : Map[String, SequenceActor],
id : String,
kind : ActorKind,
) -> Unit {
match map.get(id) {
None => {
map.set(id, { id, label: id, kind })
()
}
Some(existing) => {
if existing.kind == ActorKind::Participant && kind == ActorKind::Actor {
map.set(id, { id, label: existing.label, kind })
}
()
}
}
}
///|
fn parse_sequence(lines : Array[String], header_index : Int) -> SequenceDiagram {
let actors : Map[String, SequenceActor] = Map::new()
let messages : Array[SequenceMessage] = []
for i, line in lines {
if i <= header_index {
continue
}
let t = trim_line(line)
if t.length() == 0 || t.has_prefix("%%") {
continue
}
if t.has_prefix("participant ") || t.has_prefix("actor ") {
let parts = split_ws(t)
if parts.length() >= 2 {
let id = parts[1]
let kind = if parts[0] == "actor" {
ActorKind::Actor
} else {
ActorKind::Participant
}
ensure_actor(actors, id, kind)
}
continue
}
let arrow = if t.contains("-->>") {
"-->>"
} else if t.contains("->>") {
"->>"
} else if t.contains("-->") {
"-->"
} else if t.contains("->") {
"->"
} else {
""
}
if arrow.length() > 0 {
match split_once(t, arrow) {
Some((left_raw, right_raw)) => {
let dashed = arrow.contains("--")
let from = trim_line(left_raw)
let mut text = ""
let mut to = right_raw
match split_once(right_raw, ":") {
Some((to_raw, text_raw)) => {
to = to_raw
text = trim_line(text_raw)
}
None => ()
}
let to_id = trim_line(to)
ensure_actor(actors, from, ActorKind::Participant)
ensure_actor(actors, to_id, ActorKind::Participant)
messages.push({ from, to: to_id, text, dashed })
()
}
None => ()
}
}
}
let actors_array = Array::from_iter(actors.values())
{ actors: actors_array, messages }
}
///|
fn parse_mermaid(text : String) -> MermaidDiagram {
let lines = split_lines(text)
let (header_index, header) = first_non_empty_line(lines)
let header_lower = header.to_lower()
if header_lower.has_prefix("sequencediagram") {
Sequence(parse_sequence(lines, header_index))
} else if header_lower.has_prefix("classdiagram") {
Graph(parse_class(lines, header_index))
} else if header_lower.has_prefix("erdiagram") {
Graph(parse_er(lines, header_index))
} else if header_lower.has_prefix("statediagram") {
let direction = parse_direction_from_header(header)
Graph(parse_state(lines, header_index, direction))
} else if header_lower.has_prefix("graph") ||
header_lower.has_prefix("flowchart") {
let direction = parse_direction_from_header(header)
Graph(parse_flowchart(lines, header_index, direction))
} else {
let direction = parse_direction_from_header(header)
Graph(parse_flowchart(lines, header_index, direction))
}
}