Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/Mermaid.Flowcharts/Flowchart.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
using System.Text;
using System.Runtime.CompilerServices;
using Mermaid.Flowcharts.Links;
using Mermaid.Flowcharts.Nodes;
using Mermaid.Flowcharts.Styling;
using Mermaid.Flowcharts.Subgraphs;

[assembly: InternalsVisibleTo("Mermaid.Flowcharts.Tests")]
namespace Mermaid.Flowcharts;

public class Flowchart : IMermaidPrintable
{
private readonly List<INode> _nodes = [];
private readonly List<Link> _links = [];
internal IEnumerable<INode> AllNodeChildren => _nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodeChildren));
internal IEnumerable<Node> AllNodes => Nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodes));

public FlowchartTitle? Title { get; }
public FlowchartDirection? Direction { get; }
public IEnumerable<Node> Nodes => _nodes.OfType<Node>();
public IEnumerable<Subgraph> Subgraphs => _nodes.OfType<Subgraph>();
public IEnumerable<Link> Links => _links.AsReadOnly();
public IEnumerable<INode> AllNodeChildren => _nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodeChildren));
public IEnumerable<Node> AllNodes => Nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodes));
public IEnumerable<Link> AllLinks => Links.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllLinks));

public Flowchart(FlowchartDirection? direction = null)
{
Expand Down Expand Up @@ -59,7 +60,7 @@ public Flowchart AddLink(Link link)
public override string ToString()
=> ToMermaidString();

public string ToMermaidString(int indentations = 0, string indentationText = " ")
internal string ToMermaidStringWithOrderedLinks(int indentations, string indentationText, List<Link> linksInOrderOfOccurrence)
{
StringBuilder flowchartStringBuilder = new();
if (Title is FlowchartTitle title)
Expand All @@ -68,26 +69,32 @@ public string ToMermaidString(int indentations = 0, string indentationText = "
}
flowchartStringBuilder.AppendLine($"flowchart {Direction ?? FlowchartDirection.TD}");

// Add node declarations
foreach (Node node in Nodes)
{
flowchartStringBuilder.AppendLine(node.ToMermaidString(indentations + 1, indentationText));
}

// Add subgraph declarations
if (Subgraphs.Any())
{
flowchartStringBuilder.AppendLine();
}

foreach (Subgraph subgraph in Subgraphs)
{
flowchartStringBuilder.AppendLine(subgraph.ToMermaidString(indentations + 1, indentationText));
flowchartStringBuilder.AppendLine(subgraph.ToMermaidStringWithOrderedLinks(indentations + 1, indentationText, linksInOrderOfOccurrence));
}

// Add link declarations
if (_links.Any())
{
flowchartStringBuilder.AppendLine();
}

foreach (Link link in _links)
{
linksInOrderOfOccurrence.Add(link);
flowchartStringBuilder.AppendLine(link.ToMermaidString(indentations + 1, indentationText));
}

Expand Down Expand Up @@ -124,7 +131,7 @@ public string ToMermaidString(int indentations = 0, string indentationText = "

// Group all link styles across flowchart and all subgraphs recursively together
Dictionary<StyleClass, HashSet<int>> distinctLinkStyles = [];
foreach ((Link link, int index) in AllLinks.Select((l, i) => (l, i)))
foreach ((Link link, int index) in linksInOrderOfOccurrence.Select((l, i) => (l, i)))
{
if (link.LinkStyle is null)
{
Expand Down Expand Up @@ -154,4 +161,7 @@ public string ToMermaidString(int indentations = 0, string indentationText = "

return flowchartStringBuilder.ToString();
}

public string ToMermaidString(int indentations = 0, string indentationText = " ")
=> ToMermaidStringWithOrderedLinks(indentations, indentationText, []);
}
18 changes: 13 additions & 5 deletions src/Mermaid.Flowcharts/Subgraphs/Subgraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ public record Subgraph : INode<Subgraph>
{
private readonly List<INode> _nodes = [];
private readonly List<Link> _links = [];
internal IEnumerable<INode> AllNodeChildren => _nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodeChildren));
internal IEnumerable<Node> AllNodes => Nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodes));

public NodeIdentifier Id { get; }
public INodeText Title { get; }
public SubgraphDirection? Direction { get; }
public IEnumerable<Node> Nodes => _nodes.OfType<Node>();
public IEnumerable<Subgraph> Subgraphs => _nodes.OfType<Subgraph>();
public IEnumerable<Link> Links => _links.AsReadOnly();
public IEnumerable<INode> AllNodeChildren => _nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodeChildren));
public IEnumerable<Node> AllNodes => Nodes.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllNodes));
public IEnumerable<Link> AllLinks => Links.Concat(Subgraphs.SelectMany(subgraph => subgraph.AllLinks));

private Subgraph(NodeIdentifier id, INodeText title, SubgraphDirection? direction = null)
{
Expand Down Expand Up @@ -78,7 +77,7 @@ public Subgraph AddLink(Link link)
public override string ToString()
=> ToMermaidString();

public string ToMermaidString(int indentations = 0, string indentationText = " ")
internal string ToMermaidStringWithOrderedLinks(int indentations, string indentationText, List<Link> linksInOrderOfOccurrence)
{
StringBuilder subgraphStringBuilder = new();
string indent = indentationText.Repeat(indentations);
Expand All @@ -88,29 +87,38 @@ public string ToMermaidString(int indentations = 0, string indentationText = "
subgraphStringBuilder.AppendLine($"{indent}{indentationText}direction {Direction.Value}");
}

// Add node declarations
foreach (Node node in Nodes)
{
subgraphStringBuilder.AppendLine(node.ToMermaidString(indentations + 1, indentationText));
}

// Add subgraph declarations
if (Subgraphs.Any())
{
subgraphStringBuilder.AppendLine();
}

foreach (Subgraph subgraph in Subgraphs)
{
subgraphStringBuilder.AppendLine(subgraph.ToMermaidString(indentations + 1, indentationText));
subgraphStringBuilder.AppendLine(subgraph.ToMermaidStringWithOrderedLinks(indentations + 1, indentationText, linksInOrderOfOccurrence));
}

// Add link declarations
if (_links.Any())
{
subgraphStringBuilder.AppendLine();
}

foreach (Link link in _links)
{
linksInOrderOfOccurrence.Add(link);
subgraphStringBuilder.AppendLine(link.ToMermaidString(indentations + 1, indentationText));
}
subgraphStringBuilder.Append($"{indent}end");
return subgraphStringBuilder.ToString();
}

public string ToMermaidString(int indentations = 0, string indentationText = " ")
=> ToMermaidStringWithOrderedLinks(indentations, indentationText, []);
}
147 changes: 147 additions & 0 deletions tests/Mermaid.Flowcharts.Tests/FlowchartTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,153 @@ flowchart TD
Assert.Equal(expected, actual);
}

[Fact]
public void LinkStyleIndices_SubgraphLinkPrecedesFlowchartLink()
{
Flowchart flowchart = new();
Node a = Node.Create("a", "A");
Node b = Node.Create("b", "B");
Node c = Node.Create("c", "C");
Node d = Node.Create("d", "D");

Subgraph sg = Subgraph.Create("sg", "SG");
sg.AddNode(c).AddNode(d);
sg.AddLink(Link.Create(c, d, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#00ff00")))));

flowchart
.AddNode(a)
.AddNode(b)
.AddNode(sg)
.AddLink(Link.Create(a, b, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#ff0000")))));

string expected =
"""
flowchart TD
a["A"]
b["B"]

subgraph sg ["SG"]
c["C"]
d["D"]

c ---> d
end

a ---> b

linkStyle 0 stroke:#00ff00
linkStyle 1 stroke:#ff0000

""";

Assert.Equal(expected, flowchart.ToMermaidString(0, " "));
}

[Fact]
public void LinkStyleIndices_SharedStyleAcrossSubgraphs_AndMultipleFlowchartLinks()
{
Flowchart flowchart = new();
Node a = Node.Create("a", "A");
Node b = Node.Create("b", "B");
Node c = Node.Create("c", "C");
Node d = Node.Create("d", "D");
Node e = Node.Create("e", "E");
Node g = Node.Create("g", "G");

StyleClass green = new(Stroke: new Stroke(Color.FromHex("#00ff00")));

Subgraph sg1 = Subgraph.Create("sg1", "SG1");
sg1.AddNode(c).AddNode(d);
sg1.AddLink(Link.Create(c, d, linkStyle: green));

Subgraph sg2 = Subgraph.Create("sg2", "SG2");
sg2.AddNode(e).AddNode(g);
sg2.AddLink(Link.Create(e, g, linkStyle: green));

flowchart
.AddNode(a)
.AddNode(b)
.AddNode(sg1)
.AddNode(sg2)
.AddLink(Link.Create(a, b, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#ff0000")))))
.AddLink(Link.Create(b, a, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#0000ff")))));

string expected =
"""
flowchart TD
a["A"]
b["B"]

subgraph sg1 ["SG1"]
c["C"]
d["D"]

c ---> d
end
subgraph sg2 ["SG2"]
e["E"]
g["G"]

e ---> g
end

a ---> b
b ---> a

linkStyle 0,1 stroke:#00ff00
linkStyle 2 stroke:#ff0000
linkStyle 3 stroke:#0000ff

""";

Assert.Equal(expected, flowchart.ToMermaidString(0, " "));
}

[Fact]
public void LinkStyleIndices_NestedSubgraph_InnerLinksRenderBeforeOuterOwnLinks()
{
Flowchart flowchart = new();
Node a = Node.Create("a", "A");
Node b = Node.Create("b", "B");
Node c = Node.Create("c", "C");
Node d = Node.Create("d", "D");

Subgraph inner = Subgraph.Create("inner", "Inner");
inner.AddNode(c).AddNode(d);
inner.AddLink(Link.Create(c, d, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#00ff00")))));

Subgraph outer = Subgraph.Create("outer", "Outer");
outer.AddNode(a).AddNode(b).AddNode(inner);
outer.AddLink(Link.Create(a, b, linkStyle: new StyleClass(Stroke: new Stroke(Color.FromHex("#ff0000")))));

flowchart.AddNode(outer);

string expected =
"""
flowchart TD

subgraph outer ["Outer"]
a["A"]
b["B"]

subgraph inner ["Inner"]
c["C"]
d["D"]

c ---> d
end

a ---> b
end

linkStyle 0 stroke:#00ff00
linkStyle 1 stroke:#ff0000

""";

Assert.Equal(expected, flowchart.ToMermaidString(0, " "));
}

[Fact]
public void Flowchart_WhenNestedLinksParentToChild_ShouldNotAddDuplicateNodes()
{
Expand Down