diff --git a/src/Stateless/Graph/UmlDotGraphStyle.cs b/src/Stateless/Graph/UmlDotGraphStyle.cs
index 7d2f5bd0..f86d21d0 100644
--- a/src/Stateless/Graph/UmlDotGraphStyle.cs
+++ b/src/Stateless/Graph/UmlDotGraphStyle.cs
@@ -29,21 +29,20 @@ public override string GetPrefix()
public override string FormatOneCluster(SuperState stateInfo)
{
string stateRepresentationString = "";
- var sourceName = stateInfo.StateName;
- StringBuilder label = new StringBuilder($"{sourceName}");
+ StringBuilder label = new StringBuilder($"{EscapeLabel(stateInfo.StateName)}");
if (stateInfo.EntryActions.Count > 0 || stateInfo.ExitActions.Count > 0)
{
label.Append("\\n----------");
- label.Append(string.Concat(stateInfo.EntryActions.Select(act => "\\nentry / " + act)));
- label.Append(string.Concat(stateInfo.ExitActions.Select(act => "\\nexit / " + act)));
+ label.Append(string.Concat(stateInfo.EntryActions.Select(act => "\\nentry / " + EscapeLabel(act))));
+ label.Append(string.Concat(stateInfo.ExitActions.Select(act => "\\nexit / " + EscapeLabel(act))));
}
stateRepresentationString = "\n"
- + $"subgraph \"cluster{stateInfo.NodeName}\"" + "\n"
+ + $"subgraph \"cluster{EscapeLabel(stateInfo.NodeName)}\"" + "\n"
+ "\t{" + "\n"
- + $"\tlabel = \"{label.ToString()}\"" + "\n";
+ + $"\tlabel = \"{label}\"" + "\n";
foreach (var subState in stateInfo.SubStates)
{
@@ -62,16 +61,18 @@ public override string FormatOneCluster(SuperState stateInfo)
///
public override string FormatOneState(State state)
{
+ var escapedStateName = EscapeLabel(state.StateName);
+
if (state.EntryActions.Count == 0 && state.ExitActions.Count == 0)
- return $"\"{state.StateName}\" [label=\"{state.StateName}\"];\n";
+ return $"\"{escapedStateName}\" [label=\"{escapedStateName}\"];\n";
- string f = $"\"{state.StateName}\" [label=\"{state.StateName}|";
+ string f = $"\"{escapedStateName}\" [label=\"{escapedStateName}|";
List es = new List();
- es.AddRange(state.EntryActions.Select(act => "entry / " + act));
- es.AddRange(state.ExitActions.Select(act => "exit / " + act));
+ es.AddRange(state.EntryActions.Select(act => "entry / " + EscapeLabel(act)));
+ es.AddRange(state.ExitActions.Select(act => "exit / " + EscapeLabel(act)));
- f += String.Join("\\n", es);
+ f += string.Join("\\n", es);
f += "\"];\n";
@@ -110,7 +111,7 @@ public override string FormatOneTransition(string sourceNodeName, string trigger
///
public override string FormatOneDecisionNode(string nodeName, string label)
{
- return $"\"{nodeName}\" [shape = \"diamond\", label = \"{label}\"];\n";
+ return $"\"{EscapeLabel(nodeName)}\" [shape = \"diamond\", label = \"{EscapeLabel(label)}\"];\n";
}
///
@@ -121,17 +122,22 @@ public override string FormatOneDecisionNode(string nodeName, string label)
public override string GetInitialTransition(StateInfo initialState)
{
var initialStateName = initialState.UnderlyingState.ToString();
- string dirgraphText = System.Environment.NewLine + $" init [label=\"\", shape=point];";
- dirgraphText += System.Environment.NewLine + $" init -> \"{initialStateName}\"[style = \"solid\"]";
+ string dirgraphText = Environment.NewLine + $" init [label=\"\", shape=point];";
+ dirgraphText += Environment.NewLine + $" init -> \"{EscapeLabel(initialStateName)}\"[style = \"solid\"]";
- dirgraphText += System.Environment.NewLine + "}";
+ dirgraphText += Environment.NewLine + "}";
return dirgraphText;
}
internal string FormatOneLine(string fromNodeName, string toNodeName, string label)
{
- return $"\"{fromNodeName}\" -> \"{toNodeName}\" [style=\"solid\", label=\"{label}\"];";
+ return $"\"{EscapeLabel(fromNodeName)}\" -> \"{EscapeLabel(toNodeName)}\" [style=\"solid\", label=\"{EscapeLabel(label)}\"];";
+ }
+
+ private static string EscapeLabel(string label)
+ {
+ return label.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
}
}
diff --git a/test/Stateless.Tests/DotGraphFixture.cs b/test/Stateless.Tests/DotGraphFixture.cs
index 70b65aa3..98cfabe1 100644
--- a/test/Stateless.Tests/DotGraphFixture.cs
+++ b/test/Stateless.Tests/DotGraphFixture.cs
@@ -136,19 +136,32 @@ public void SimpleTransition()
}
[Fact]
- public void SimpleTransitionUML()
+ public void SimpleTransitionWithEscaping()
{
- var expected = Prefix(Style.UML) + Box(Style.UML, "A") + Box(Style.UML, "B") + Line("A", "B", "X") + suffix;
+ var state1 = "\\state \"1\"";
+ var state2 = "\\state \"2\"";
+ var trigger1 = "\\trigger \"1\"";
- var sm = new StateMachine(State.A);
+ string suffix = Environment.NewLine
+ + $" init [label=\"\", shape=point];" + Environment.NewLine
+ + $" init -> \"{EscapeLabel(state1)}\"[style = \"solid\"]" + Environment.NewLine
+ + "}";
- sm.Configure(State.A)
- .Permit(Trigger.X, State.B);
+ var expected =
+ Prefix(Style.UML) +
+ Box(Style.UML, EscapeLabel(state1)) +
+ Box(Style.UML, EscapeLabel(state2)) +
+ Line(EscapeLabel(state1), EscapeLabel(state2), EscapeLabel(trigger1)) + suffix;
+
+ var sm = new StateMachine(state1);
+
+ sm.Configure(state1)
+ .Permit(trigger1, state2);
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
#if WRITE_DOTS_TO_FOLDER
- System.IO.File.WriteAllText(DestinationFolder + "SimpleTransitionUML.dot", dotGraph);
+ System.IO.File.WriteAllText(DestinationFolder + "SimpleTransitionWithEscaping.dot", dotGraph);
#endif
Assert.Equal(expected, dotGraph);
@@ -196,7 +209,7 @@ public void WhenDiscriminatedByAnonymousGuardWithDescription()
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A") + Box(Style.UML, "B")
+ Line("A", "B", "X [description]")
- + suffix;
+ + suffix;
var sm = new StateMachine(State.A);
@@ -398,46 +411,50 @@ public void OnEntryWithTriggerParameter()
Assert.Equal(expected, dotGraph);
}
-
+
[Fact]
public void SpacedUmlWithSubstate()
{
- string StateA = "State A";
- string StateB = "State B";
- string StateC = "State C";
- string StateD = "State D";
- string TriggerX = "Trigger X";
- string TriggerY = "Trigger Y";
-
+ string StateA = "State \"A\"";
+ string StateB = "State \"B\"";
+ string StateC = "State \"C\"";
+ string StateD = "State \"D\"";
+ string TriggerX = "Trigger \"X\"";
+ string TriggerY = "Trigger \"Y\"";
+ string EnterA = "Enter \"A\"";
+ string EnterD = "Enter \"D\"";
+ string ExitA = "Exit \"A\"";
+
var expected = Prefix(Style.UML)
- + Subgraph(Style.UML, StateD, $"{StateD}\\n----------\\nentry / Enter D",
- Box(Style.UML, StateB)
- + Box(Style.UML, StateC))
- + Box(Style.UML, StateA, new List { "Enter A" }, new List { "Exit A" })
- + Line(StateA, StateB, TriggerX) + Line(StateA, StateC, TriggerY)
- + Environment.NewLine
+ + Subgraph(Style.UML, EscapeLabel(StateD), $"{EscapeLabel(StateD)}\\n----------\\nentry / {EscapeLabel(EnterD)}",
+ Box(Style.UML, EscapeLabel(StateB))
+ + Box(Style.UML, EscapeLabel(StateC)))
+ + Box(Style.UML, EscapeLabel(StateA), new List { EscapeLabel(EnterA) }, new List { EscapeLabel(ExitA) })
+ + Line(EscapeLabel(StateA), EscapeLabel(StateB), EscapeLabel(TriggerX))
+ + Line(EscapeLabel(StateA), EscapeLabel(StateC), EscapeLabel(TriggerY))
+ + Environment.NewLine
+ $" init [label=\"\", shape=point];" + Environment.NewLine
- + $" init -> \"{StateA}\"[style = \"solid\"]" + Environment.NewLine
+ + $" init -> \"{EscapeLabel(StateA)}\"[style = \"solid\"]" + Environment.NewLine
+ "}";
- var sm = new StateMachine("State A");
+ var sm = new StateMachine(StateA);
sm.Configure(StateA)
.Permit(TriggerX, StateB)
.Permit(TriggerY, StateC)
- .OnEntry(TestEntryAction, "Enter A")
- .OnExit(TestEntryAction, "Exit A");
+ .OnEntry(TestEntryAction, EnterA)
+ .OnExit(TestEntryAction, ExitA);
sm.Configure(StateB)
.SubstateOf(StateD);
sm.Configure(StateC)
.SubstateOf(StateD);
sm.Configure(StateD)
- .OnEntry(TestEntryAction, "Enter D");
+ .OnEntry(TestEntryAction, EnterD);
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
#if WRITE_DOTS_TO_FOLDER
- System.IO.File.WriteAllText(DestinationFolder + "UmlWithSubstate.dot", dotGraph);
+ System.IO.File.WriteAllText(DestinationFolder + "SpacedUmlWithSubstate.dot", dotGraph);
#endif
Assert.Equal(expected, dotGraph);
@@ -493,7 +510,7 @@ public void UmlWithDynamic()
var sm = new StateMachine(State.A);
sm.Configure(State.A)
- .PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB"}, { State.C, "ChoseC" } });
+ .PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });
sm.Configure(State.B);
sm.Configure(State.C);
@@ -513,7 +530,7 @@ public void TransitionWithIgnoreAndEntry()
+ Box(Style.UML, "A", new List { "DoEntry" })
+ Box(Style.UML, "B", new List { "DoThisEntry" })
+ Line("A", "B", "X")
- + Line("A", "A", "Y")
+ + Line("A", "A", "Y")
+ Line("B", "B", "Z / DoThisEntry")
+ suffix;
@@ -644,5 +661,6 @@ public void Reentrant_Transition_Shows_Entry_Action_When_Action_Is_Configured_Wi
private void TestEntryAction() { }
private void TestEntryActionString(string val) { }
private State DestinationSelector() { return State.A; }
+ private static string EscapeLabel(string label) { return label.Replace("\\", "\\\\").Replace("\"", "\\\""); }
}
}