diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f5eec7e..a517cbd 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,10 +53,16 @@ jobs:
# queries: security-extended,security-and-quality
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore src/FluentMermaidLibrary.sln -nologo -v minimal
+
+ - name: Build
+ run: dotnet build src/FluentMermaidLibrary.sln --no-restore -nologo -v minimal
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +75,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 6b82d58..47bd354 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -21,8 +21,8 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Restore dependencies
- run: dotnet restore src/FluentMermaidLibrary.sln
+ run: dotnet restore src/FluentMermaidLibrary.sln -nologo -v minimal
- name: Build
- run: dotnet build --no-restore src/FluentMermaidLibrary.sln
+ run: dotnet build --no-restore src/FluentMermaidLibrary.sln -nologo -v minimal
- name: Test
- run: dotnet test src/FluentMermaidLibrary.sln --no-build --verbosity normal
+ run: dotnet test src/FluentMermaidLibrary.sln --no-build -nologo -v minimal
diff --git a/README.md b/README.md
index 3ae3aa9..2128e06 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,61 @@
# FluentMermaid
-

-.NET api for generating mermaid syntax markdown that then could be rendered with [Mermaid.js](https://mermaid-js.github.io/mermaid/#/)
+.NET api for generating mermaid syntax markdown that then could be rendered with [Mermaid.js](https://mermaid.js.org/)
Supported platforms: `.netstandard 2.0`
-Tested with Mermaid js v9.1.1
+Tested with Mermaid js [v11.12.3](https://github.com/mermaid-js/mermaid/releases/tag/mermaid%4011.12.3)
### For examples look at [WIKI repo page](https://github.com/wowbios/FluentMermaid/wiki/)
+## Flowchart API status (Mermaid v11)
+
+- Backward compatible API: `FlowChart.Create(...)` and existing `TextNode/Link/SubGraph/Interaction/Styling`.
+- Advanced additive API: `FlowChart.CreateAdvanced(...)`.
+
+### Coverage matrix
+
+- Legacy shapes (`[]`, `()`, `(( ))`, `{{ }}` etc): supported.
+- Expanded shapes syntax (`A@{ shape: ... }`): supported via `CreateAdvanced` + `TextNode(string, string shapeAlias)`.
+- Special shapes `icon` / `image`: supported via `IconNode(...)` / `ImageNode(...)`.
+- Edge IDs (`e1@-->`) and edge-level options (`animate/animation/curve`): supported via advanced `Link(... edgeId ...)` + `EdgeStyling`.
+- Link styles (`linkStyle` index/default): supported via `EdgeStyling`.
+
+### Quick examples
+
+```csharp
+using FluentMermaid.Enums;
+using FluentMermaid.Flowchart;
+using FluentMermaid.Flowchart.Enum;
+
+var chart = FlowChart.CreateAdvanced(Orientation.LeftToRight);
+
+var a = chart.TextNode("Manual input", AdvancedShape.SlopedRectangle);
+var b = chart.IconNode("fa:user", "User Icon", "square", "t", 60);
+var c = chart.ImageNode(new Uri("https://example.com/image.png"), "Image", "t", 60, 60, false);
+
+chart.Link(a, b, "e1", Link.Arrow, "", 1);
+chart.Link(b, c, Link.Thick, "next", 1); // old API still works
+
+chart.EdgeStyling.SetAnimated("e1");
+chart.EdgeStyling.SetAnimation("e1", EdgeAnimationSpeed.Fast);
+chart.EdgeStyling.SetCurve("e1", EdgeCurve.Linear);
+chart.EdgeStyling.LinkStyleDefault("color:blue");
+
+var mermaid = chart.Render();
+```
+
# Roadmap
-- [x] [Flowchart](https://mermaid-js.github.io/mermaid/#/flowchart)
-- [x] [Sequence diagram](https://mermaid-js.github.io/mermaid/#/sequenceDiagram)
-- [x] [Class diagram](https://mermaid-js.github.io/mermaid/#/classDiagram)
-- [x] [Pie chart](https://mermaid-js.github.io/mermaid/#/pie)
-- [x] [State diagram](https://mermaid-js.github.io/mermaid/#/stateDiagram)
-- [ ] [Entity relationship](https://mermaid-js.github.io/mermaid/#/entityRelationshipDiagram) https://github.com/wowbios/FluentMermaid/issues/18
-- [ ] [User journey](https://mermaid-js.github.io/mermaid/#/user-journey) https://github.com/wowbios/FluentMermaid/issues/19
-- [ ] [Gantt](https://mermaid-js.github.io/mermaid/#/gantt) https://github.com/wowbios/FluentMermaid/issues/20
-- [ ] [Requirement](https://mermaid-js.github.io/mermaid/#/requirementDiagram) https://github.com/wowbios/FluentMermaid/issues/21
-- [ ] [Git graph](https://mermaid-js.github.io/mermaid/#/gitgraph) https://github.com/wowbios/FluentMermaid/issues/22
+- [x] [Flowchart](https://mermaid.js.org/syntax/flowchart.html)
+- [x] [Sequence diagram](https://mermaid.js.org/syntax/sequenceDiagram.html)
+- [x] [Class diagram](https://mermaid.js.org/syntax/classDiagram.html)
+- [x] [Pie chart](https://mermaid.js.org/syntax/pie.html)
+- [x] [State diagram](https://mermaid.js.org/syntax/stateDiagram.html)
+- [ ] [Entity relationship](https://mermaid.js.org/syntax/entityRelationshipDiagram.html) https://github.com/wowbios/FluentMermaid/issues/18
+- [ ] [User journey](https://mermaid.js.org/syntax/userJourney.html) https://github.com/wowbios/FluentMermaid/issues/19
+- [ ] [Gantt](https://mermaid.js.org/syntax/gantt.html) https://github.com/wowbios/FluentMermaid/issues/20
+- [ ] [Requirement](https://mermaid.js.org/syntax/requirementDiagram.html) https://github.com/wowbios/FluentMermaid/issues/21
+- [ ] [Git graph](https://mermaid.js.org/syntax/gitgraph.html) https://github.com/wowbios/FluentMermaid/issues/22
- [ ] Flowchart fluent API https://github.com/wowbios/FluentMermaid/issues/15
- [ ] Class diagram fluent API https://github.com/wowbios/FluentMermaid/issues/16
diff --git a/src/FluentMermaid/Flowchart/AdvancedShape.cs b/src/FluentMermaid/Flowchart/AdvancedShape.cs
new file mode 100644
index 0000000..133a826
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/AdvancedShape.cs
@@ -0,0 +1,52 @@
+namespace FluentMermaid.Flowchart;
+
+public static class AdvancedShape
+{
+ public const string Bang = "bang";
+ public const string NotchedRectangle = "notch-rect";
+ public const string Cloud = "cloud";
+ public const string Hourglass = "hourglass";
+ public const string Bolt = "bolt";
+ public const string BraceLeft = "brace-l";
+ public const string BraceRight = "brace-r";
+ public const string Braces = "braces";
+ public const string LeanRight = "lean-r";
+ public const string LeanLeft = "lean-l";
+ public const string Cylinder = "cyl";
+ public const string Diamond = "diam";
+ public const string Delay = "delay";
+ public const string HorizontalCylinder = "h-cyl";
+ public const string LinedCylinder = "lin-cyl";
+ public const string CurvedTrapezoid = "curv-trap";
+ public const string DividedRectangle = "div-rect";
+ public const string Document = "doc";
+ public const string RoundedRectangle = "rounded";
+ public const string Triangle = "tri";
+ public const string Fork = "fork";
+ public const string WindowPane = "win-pane";
+ public const string FilledCircle = "f-circ";
+ public const string LinedDocument = "lin-doc";
+ public const string LinedRectangle = "lin-rect";
+ public const string NotchedPentagon = "notch-pent";
+ public const string FlippedTriangle = "flip-tri";
+ public const string SlopedRectangle = "sl-rect";
+ public const string InvertedTrapezoid = "trap-t";
+ public const string StackedDocuments = "docs";
+ public const string StackedRectangle = "st-rect";
+ public const string Odd = "odd";
+ public const string Flag = "flag";
+ public const string Hexagon = "hex";
+ public const string Trapezoid = "trap-b";
+ public const string Rectangle = "rect";
+ public const string Circle = "circle";
+ public const string SmallCircle = "sm-circ";
+ public const string DoubleCircle = "dbl-circ";
+ public const string FramedCircle = "fr-circ";
+ public const string BowTieRectangle = "bow-rect";
+ public const string FramedRectangle = "fr-rect";
+ public const string CrossedCircle = "cross-circ";
+ public const string TaggedDocument = "tag-doc";
+ public const string TaggedRectangle = "tag-rect";
+ public const string Stadium = "stadium";
+ public const string Text = "text";
+}
diff --git a/src/FluentMermaid/Flowchart/Enum/EdgeAnimationSpeed.cs b/src/FluentMermaid/Flowchart/Enum/EdgeAnimationSpeed.cs
new file mode 100644
index 0000000..d4fcd60
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Enum/EdgeAnimationSpeed.cs
@@ -0,0 +1,7 @@
+namespace FluentMermaid.Flowchart.Enum;
+
+public enum EdgeAnimationSpeed
+{
+ Fast,
+ Slow
+}
diff --git a/src/FluentMermaid/Flowchart/Enum/EdgeCurve.cs b/src/FluentMermaid/Flowchart/Enum/EdgeCurve.cs
new file mode 100644
index 0000000..97e1b26
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Enum/EdgeCurve.cs
@@ -0,0 +1,17 @@
+namespace FluentMermaid.Flowchart.Enum;
+
+public enum EdgeCurve
+{
+ Basis,
+ BumpX,
+ BumpY,
+ Cardinal,
+ CatmullRom,
+ Linear,
+ MonotoneX,
+ MonotoneY,
+ Natural,
+ Step,
+ StepAfter,
+ StepBefore
+}
diff --git a/src/FluentMermaid/Flowchart/Enum/Link.cs b/src/FluentMermaid/Flowchart/Enum/Link.cs
index 2e84cd1..adec26a 100644
--- a/src/FluentMermaid/Flowchart/Enum/Link.cs
+++ b/src/FluentMermaid/Flowchart/Enum/Link.cs
@@ -1,4 +1,4 @@
-namespace FluentMermaid.Flowchart.Enum;
+namespace FluentMermaid.Flowchart.Enum;
public enum Link
{
@@ -16,11 +16,21 @@ public enum Link
/// -.-
///
Dotted,
+
+ ///
+ /// -.->
+ ///
+ DottedArrow,
///
/// ==>
///
Thick,
+
+ ///
+ /// ===
+ ///
+ ThickOpen,
///
/// --o
@@ -46,4 +56,9 @@ public enum Link
/// x--x
///
CrossDouble,
+
+ ///
+ /// ~~~
+ ///
+ Invisible,
}
\ No newline at end of file
diff --git a/src/FluentMermaid/Flowchart/Enum/Shape.cs b/src/FluentMermaid/Flowchart/Enum/Shape.cs
index cf1e849..a41cd54 100644
--- a/src/FluentMermaid/Flowchart/Enum/Shape.cs
+++ b/src/FluentMermaid/Flowchart/Enum/Shape.cs
@@ -1,7 +1,8 @@
-namespace FluentMermaid.Flowchart.Enum;
+namespace FluentMermaid.Flowchart.Enum;
public enum Shape
{
+ Rectangle,
RoundEdges,
Stadium,
Subroutine,
diff --git a/src/FluentMermaid/Flowchart/Extensions/EdgeAnimationSpeedExtensions.cs b/src/FluentMermaid/Flowchart/Extensions/EdgeAnimationSpeedExtensions.cs
new file mode 100644
index 0000000..83b7d09
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Extensions/EdgeAnimationSpeedExtensions.cs
@@ -0,0 +1,14 @@
+using FluentMermaid.Flowchart.Enum;
+
+namespace FluentMermaid.Flowchart.Extensions;
+
+internal static class EdgeAnimationSpeedExtensions
+{
+ public static string Render(this EdgeAnimationSpeed speed)
+ => speed switch
+ {
+ EdgeAnimationSpeed.Fast => "fast",
+ EdgeAnimationSpeed.Slow => "slow",
+ _ => throw new ArgumentOutOfRangeException(nameof(speed), speed, null)
+ };
+}
diff --git a/src/FluentMermaid/Flowchart/Extensions/EdgeCurveExtensions.cs b/src/FluentMermaid/Flowchart/Extensions/EdgeCurveExtensions.cs
new file mode 100644
index 0000000..298eefd
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Extensions/EdgeCurveExtensions.cs
@@ -0,0 +1,24 @@
+using FluentMermaid.Flowchart.Enum;
+
+namespace FluentMermaid.Flowchart.Extensions;
+
+internal static class EdgeCurveExtensions
+{
+ public static string Render(this EdgeCurve curve)
+ => curve switch
+ {
+ EdgeCurve.Basis => "basis",
+ EdgeCurve.BumpX => "bumpX",
+ EdgeCurve.BumpY => "bumpY",
+ EdgeCurve.Cardinal => "cardinal",
+ EdgeCurve.CatmullRom => "catmullRom",
+ EdgeCurve.Linear => "linear",
+ EdgeCurve.MonotoneX => "monotoneX",
+ EdgeCurve.MonotoneY => "monotoneY",
+ EdgeCurve.Natural => "natural",
+ EdgeCurve.Step => "step",
+ EdgeCurve.StepAfter => "stepAfter",
+ EdgeCurve.StepBefore => "stepBefore",
+ _ => throw new ArgumentOutOfRangeException(nameof(curve), curve, null)
+ };
+}
diff --git a/src/FluentMermaid/Flowchart/Extensions/LinkExtensions.cs b/src/FluentMermaid/Flowchart/Extensions/LinkExtensions.cs
index 5fce996..8364502 100644
--- a/src/FluentMermaid/Flowchart/Extensions/LinkExtensions.cs
+++ b/src/FluentMermaid/Flowchart/Extensions/LinkExtensions.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using FluentMermaid.Flowchart.Enum;
namespace FluentMermaid.Flowchart.Extensions;
@@ -19,9 +19,15 @@ public static void RenderTo(
RenderSingle('-', "--");
break;
case Link.Dotted:
- RenderDouble("-.", '.', "-");
+ RenderDouble("-", '.', "-");
+ break;
+ case Link.DottedArrow:
+ RenderDouble("-", '.', "->");
break;
case Link.Thick:
+ RenderSingle('=', "=>");
+ break;
+ case Link.ThickOpen:
RenderSingle('=', "==");
break;
case Link.Circle:
@@ -38,6 +44,11 @@ public static void RenderTo(
break;
case Link.CrossDouble:
RenderDouble("x", '-', "-x");
+ break;
+ case Link.Invisible:
+ for (var i = 0; i < length + 2; i++)
+ builder.Append('~');
+
break;
default:
throw new ArgumentOutOfRangeException(nameof(link), link, null);
diff --git a/src/FluentMermaid/Flowchart/Extensions/ShapeExtensions.cs b/src/FluentMermaid/Flowchart/Extensions/ShapeExtensions.cs
index a7dd07c..510df5f 100644
--- a/src/FluentMermaid/Flowchart/Extensions/ShapeExtensions.cs
+++ b/src/FluentMermaid/Flowchart/Extensions/ShapeExtensions.cs
@@ -1,4 +1,4 @@
-using FluentMermaid.Flowchart.Enum;
+using FluentMermaid.Flowchart.Enum;
namespace FluentMermaid.Flowchart.Extensions;
@@ -7,6 +7,7 @@ internal static class ShapeExtensions
public static string RenderStart(this Shape shape)
=> shape switch
{
+ Shape.Rectangle => "[",
Shape.RoundEdges => "(",
Shape.Stadium => "([",
Shape.Subroutine => "[[",
@@ -26,6 +27,7 @@ public static string RenderStart(this Shape shape)
public static string RenderEnd(this Shape shape)
=> shape switch
{
+ Shape.Rectangle => "]",
Shape.RoundEdges => ")",
Shape.Stadium => "])",
Shape.Subroutine => "]]",
diff --git a/src/FluentMermaid/Flowchart/FlowChart.cs b/src/FluentMermaid/Flowchart/FlowChart.cs
index ccb8042..5349eaf 100644
--- a/src/FluentMermaid/Flowchart/FlowChart.cs
+++ b/src/FluentMermaid/Flowchart/FlowChart.cs
@@ -1,4 +1,4 @@
-using FluentMermaid.Enums;
+using FluentMermaid.Enums;
using FluentMermaid.Flowchart.Interfaces;
using FluentMermaid.Flowchart.Nodes;
@@ -7,4 +7,6 @@ namespace FluentMermaid.Flowchart;
public static class FlowChart
{
public static IFlowChart Create(Orientation orientation) => new FlowchartRootNode(orientation);
+
+ public static IFlowChartAdvanced CreateAdvanced(Orientation orientation) => new FlowchartRootNode(orientation);
}
\ No newline at end of file
diff --git a/src/FluentMermaid/Flowchart/Interfaces/IAdvancedGraph.cs b/src/FluentMermaid/Flowchart/Interfaces/IAdvancedGraph.cs
new file mode 100644
index 0000000..7fe599c
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Interfaces/IAdvancedGraph.cs
@@ -0,0 +1,36 @@
+using FluentMermaid.Enums;
+using FluentMermaid.Flowchart.Enum;
+
+namespace FluentMermaid.Flowchart.Interfaces;
+
+public interface IAdvancedGraph : IGraph
+{
+ INode TextNode(string content, string shapeAlias);
+
+ INode IconNode(
+ string icon,
+ string? label = null,
+ string? form = null,
+ string? position = null,
+ int? height = null);
+
+ INode ImageNode(
+ Uri imageUrl,
+ string? label = null,
+ string? position = null,
+ int? width = null,
+ int? height = null,
+ bool? constraint = null);
+
+ IAdvancedSubGraph AdvancedSubGraph(string title, Orientation orientation);
+
+ IAdvancedSubGraph AdvancedSubGraph(string id, string title, Orientation orientation);
+
+ void Link(
+ INode from,
+ INode to,
+ string edgeId,
+ Link link,
+ string text,
+ int length = 1);
+}
diff --git a/src/FluentMermaid/Flowchart/Interfaces/IAdvancedSubGraph.cs b/src/FluentMermaid/Flowchart/Interfaces/IAdvancedSubGraph.cs
new file mode 100644
index 0000000..8162751
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Interfaces/IAdvancedSubGraph.cs
@@ -0,0 +1,5 @@
+namespace FluentMermaid.Flowchart.Interfaces;
+
+public interface IAdvancedSubGraph : ISubGraph, IAdvancedGraph
+{
+}
diff --git a/src/FluentMermaid/Flowchart/Interfaces/IEdgeStyling.cs b/src/FluentMermaid/Flowchart/Interfaces/IEdgeStyling.cs
new file mode 100644
index 0000000..d64499f
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Interfaces/IEdgeStyling.cs
@@ -0,0 +1,20 @@
+using FluentMermaid.Flowchart.Enum;
+
+namespace FluentMermaid.Flowchart.Interfaces;
+
+public interface IEdgeStyling : INode
+{
+ void SetAnimated(string edgeId, bool value = true);
+
+ void SetAnimation(string edgeId, EdgeAnimationSpeed speed);
+
+ void SetCurve(string edgeId, EdgeCurve curve);
+
+ void SetClass(string edgeId, string className);
+
+ void LinkStyle(int linkIndex, string css);
+
+ void LinkStyle(IEnumerable linkIndexes, string css);
+
+ void LinkStyleDefault(string css);
+}
diff --git a/src/FluentMermaid/Flowchart/Interfaces/IFlowChartAdvanced.cs b/src/FluentMermaid/Flowchart/Interfaces/IFlowChartAdvanced.cs
new file mode 100644
index 0000000..fb57648
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Interfaces/IFlowChartAdvanced.cs
@@ -0,0 +1,6 @@
+namespace FluentMermaid.Flowchart.Interfaces;
+
+public interface IFlowChartAdvanced : IFlowChart, IAdvancedGraph
+{
+ IEdgeStyling EdgeStyling { get; }
+}
diff --git a/src/FluentMermaid/Flowchart/Nodes/AdvancedTextNode.cs b/src/FluentMermaid/Flowchart/Nodes/AdvancedTextNode.cs
new file mode 100644
index 0000000..0546176
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Nodes/AdvancedTextNode.cs
@@ -0,0 +1,70 @@
+using System.Globalization;
+using System.Text;
+using FluentMermaid.Extensions;
+using FluentMermaid.Flowchart.Interfaces;
+
+namespace FluentMermaid.Flowchart.Nodes;
+
+internal sealed record AdvancedTextNode : Node
+{
+ private readonly List _attributes = new();
+
+ public AdvancedTextNode(
+ IGraph graph,
+ string id,
+ params NodeAttribute[] attributes)
+ : base(graph, id)
+ {
+ _attributes.AddRange(attributes);
+ }
+
+ public override void RenderTo(StringBuilder builder)
+ {
+ builder.Append(Id).Append("@{ ");
+
+ var hasWrittenAttribute = false;
+ foreach (NodeAttribute attribute in _attributes)
+ {
+ if (hasWrittenAttribute)
+ builder.Append(", ");
+
+ builder.Append(attribute.Key).Append(": ");
+ if (attribute.QuoteValue)
+ {
+ builder.Append('"');
+ builder.WriteEscaped(attribute.Value);
+ builder.Append('"');
+ }
+ else
+ {
+ builder.Append(attribute.Value);
+ }
+
+ hasWrittenAttribute = true;
+ }
+
+ builder.AppendLine(" }");
+ }
+
+ internal readonly struct NodeAttribute
+ {
+ public NodeAttribute(string key, string value, bool quoteValue = false)
+ {
+ Key = key;
+ Value = value;
+ QuoteValue = quoteValue;
+ }
+
+ public string Key { get; }
+
+ public string Value { get; }
+
+ public bool QuoteValue { get; }
+
+ public static NodeAttribute FromBool(string key, bool value)
+ => new(key, value ? "true" : "false");
+
+ public static NodeAttribute FromInt(string key, int value)
+ => new(key, value.ToString(CultureInfo.InvariantCulture));
+ }
+}
diff --git a/src/FluentMermaid/Flowchart/Nodes/EdgeStylingNode.cs b/src/FluentMermaid/Flowchart/Nodes/EdgeStylingNode.cs
new file mode 100644
index 0000000..278d2fb
--- /dev/null
+++ b/src/FluentMermaid/Flowchart/Nodes/EdgeStylingNode.cs
@@ -0,0 +1,142 @@
+using System.Text;
+using FluentMermaid.Flowchart.Enum;
+using FluentMermaid.Flowchart.Extensions;
+using FluentMermaid.Flowchart.Interfaces;
+
+namespace FluentMermaid.Flowchart.Nodes;
+
+internal sealed class EdgeStylingNode : IEdgeStyling
+{
+ private readonly Dictionary _edgeProperties = new();
+ private readonly List _edgePropertiesOrder = new();
+ private readonly List<(string edgeId, string className)> _edgeClasses = new();
+ private readonly List<(string selector, string css)> _linkStyles = new();
+
+ public string Id { get; } = "edgeStyling";
+
+ public void SetAnimated(string edgeId, bool value = true)
+ => GetOrCreate(edgeId).Animate = value;
+
+ public void SetAnimation(string edgeId, EdgeAnimationSpeed speed)
+ => GetOrCreate(edgeId).Animation = speed;
+
+ public void SetCurve(string edgeId, EdgeCurve curve)
+ => GetOrCreate(edgeId).Curve = curve;
+
+ public void SetClass(string edgeId, string className)
+ {
+ EnsureEdgeId(edgeId);
+ if (string.IsNullOrWhiteSpace(className))
+ throw new ArgumentException("Class name should not be null or empty", nameof(className));
+
+ _edgeClasses.Add((edgeId, className));
+ }
+
+ public void LinkStyle(int linkIndex, string css)
+ {
+ if (linkIndex < 0)
+ throw new ArgumentException("Link index should be non-negative", nameof(linkIndex));
+ EnsureCss(css);
+
+ _linkStyles.Add((linkIndex.ToString(), css));
+ }
+
+ public void LinkStyle(IEnumerable linkIndexes, string css)
+ {
+ if (linkIndexes is null)
+ throw new ArgumentNullException(nameof(linkIndexes));
+ EnsureCss(css);
+
+ var indexes = linkIndexes.ToArray();
+ if (indexes.Length == 0)
+ throw new ArgumentException("At least one link index should be provided", nameof(linkIndexes));
+ if (indexes.Any(i => i < 0))
+ throw new ArgumentException("All link indexes should be non-negative", nameof(linkIndexes));
+
+ _linkStyles.Add((string.Join(",", indexes), css));
+ }
+
+ public void LinkStyleDefault(string css)
+ {
+ EnsureCss(css);
+ _linkStyles.Add(("default", css));
+ }
+
+ public void RenderTo(StringBuilder builder)
+ {
+ foreach (string edgeId in _edgePropertiesOrder)
+ {
+ EdgeProperties props = _edgeProperties[edgeId];
+ var pairs = new List(3);
+
+ if (props.Animate.HasValue)
+ pairs.Add($"animate: {(props.Animate.Value ? "true" : "false")}");
+ if (props.Animation.HasValue)
+ pairs.Add($"animation: {props.Animation.Value.Render()}");
+ if (props.Curve.HasValue)
+ pairs.Add($"curve: {props.Curve.Value.Render()}");
+
+ if (pairs.Count == 0)
+ continue;
+
+ builder
+ .Append(edgeId)
+ .Append("@{ ")
+ .Append(string.Join(", ", pairs))
+ .AppendLine(" }");
+ }
+
+ foreach ((string edgeId, string className) in _edgeClasses)
+ {
+ builder
+ .Append("class ")
+ .Append(edgeId)
+ .Append(' ')
+ .AppendLine(className);
+ }
+
+ foreach ((string selector, string css) in _linkStyles)
+ {
+ builder
+ .Append("linkStyle ")
+ .Append(selector)
+ .Append(' ')
+ .Append(css)
+ .AppendLine(";");
+ }
+ }
+
+ private EdgeProperties GetOrCreate(string edgeId)
+ {
+ EnsureEdgeId(edgeId);
+
+ if (_edgeProperties.TryGetValue(edgeId, out EdgeProperties? value))
+ return value;
+
+ var props = new EdgeProperties();
+ _edgeProperties[edgeId] = props;
+ _edgePropertiesOrder.Add(edgeId);
+ return props;
+ }
+
+ private static void EnsureEdgeId(string edgeId)
+ {
+ if (string.IsNullOrWhiteSpace(edgeId))
+ throw new ArgumentException("Edge id should not be null or empty", nameof(edgeId));
+ }
+
+ private static void EnsureCss(string css)
+ {
+ if (string.IsNullOrWhiteSpace(css))
+ throw new ArgumentException("CSS should not be null or empty", nameof(css));
+ }
+
+ private sealed class EdgeProperties
+ {
+ public bool? Animate { get; set; }
+
+ public EdgeAnimationSpeed? Animation { get; set; }
+
+ public EdgeCurve? Curve { get; set; }
+ }
+}
diff --git a/src/FluentMermaid/Flowchart/Nodes/FlowchartRootNode.cs b/src/FluentMermaid/Flowchart/Nodes/FlowchartRootNode.cs
index e83cd93..ba6d35f 100644
--- a/src/FluentMermaid/Flowchart/Nodes/FlowchartRootNode.cs
+++ b/src/FluentMermaid/Flowchart/Nodes/FlowchartRootNode.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using FluentMermaid.Enums;
using FluentMermaid.Extensions;
using FluentMermaid.Flowchart.Enum;
@@ -8,7 +8,7 @@
namespace FluentMermaid.Flowchart.Nodes;
-internal class FlowchartRootNode : IFlowChart
+internal class FlowchartRootNode : IFlowChartAdvanced
{
internal FlowchartRootNode(Orientation orientation)
{
@@ -21,9 +21,11 @@ internal FlowchartRootNode(Orientation orientation)
public IStyling Styling { get; } = new Styling.Styling("styling");
- private HashSet Nodes { get; } = new();
+ public IEdgeStyling EdgeStyling { get; } = new EdgeStylingNode();
- private HashSet Relations { get; } = new();
+ private List Nodes { get; } = new();
+
+ private List Relations { get; } = new();
public INode TextNode(string content, Shape shape)
{
@@ -39,18 +41,104 @@ public ISubGraph SubGraph(string title, Orientation orientation)
return subgraph;
}
+ public IAdvancedSubGraph AdvancedSubGraph(string title, Orientation orientation)
+ {
+ var subgraph = new SubGraphNode(CreateNodeId(), title, orientation);
+ Nodes.Add(subgraph);
+ return subgraph;
+ }
+
+ public IAdvancedSubGraph AdvancedSubGraph(string id, string title, Orientation orientation)
+ {
+ var subgraph = new SubGraphNode(id, title, orientation);
+ Nodes.Add(subgraph);
+ return subgraph;
+ }
+
public void Link(
INode from,
INode to,
Link link,
string text,
int length = 1)
+ => AddRelation(from, to, link, text, length);
+
+ public void Link(
+ INode from,
+ INode to,
+ string edgeId,
+ Link link,
+ string text,
+ int length = 1)
+ => AddRelation(from, to, link, text, length, edgeId);
+
+ public INode TextNode(string content, string shapeAlias)
{
- if (length < 1)
- throw new ArgumentException("Link length should be more or equal 1", nameof(length));
+ if (string.IsNullOrWhiteSpace(shapeAlias))
+ throw new ArgumentException("Shape alias should not be null or empty", nameof(shapeAlias));
- Relation relation = new(from, to, link, text, length);
- Relations.Add(relation);
+ AdvancedTextNode textNode = new(
+ this,
+ CreateNodeId(),
+ new AdvancedTextNode.NodeAttribute("shape", shapeAlias),
+ new AdvancedTextNode.NodeAttribute("label", content, true));
+
+ Nodes.Add(textNode);
+ return textNode;
+ }
+
+ public INode IconNode(string icon, string? label = null, string? form = null, string? position = null, int? height = null)
+ {
+ if (string.IsNullOrWhiteSpace(icon))
+ throw new ArgumentException("Icon should not be null or empty", nameof(icon));
+
+ var attrs = new List
+ {
+ new("icon", icon, true)
+ };
+ if (!string.IsNullOrWhiteSpace(form))
+ attrs.Add(new("form", form!, true));
+ if (!string.IsNullOrWhiteSpace(label))
+ attrs.Add(new("label", label!, true));
+ if (!string.IsNullOrWhiteSpace(position))
+ attrs.Add(new("pos", position!, true));
+ if (height.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("h", height.Value));
+
+ AdvancedTextNode node = new(this, CreateNodeId(), attrs.ToArray());
+ Nodes.Add(node);
+ return node;
+ }
+
+ public INode ImageNode(
+ Uri imageUrl,
+ string? label = null,
+ string? position = null,
+ int? width = null,
+ int? height = null,
+ bool? constraint = null)
+ {
+ if (imageUrl is null)
+ throw new ArgumentNullException(nameof(imageUrl));
+
+ var attrs = new List
+ {
+ new("img", imageUrl.ToString(), true)
+ };
+ if (!string.IsNullOrWhiteSpace(label))
+ attrs.Add(new("label", label!, true));
+ if (!string.IsNullOrWhiteSpace(position))
+ attrs.Add(new("pos", position!, true));
+ if (width.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("w", width.Value));
+ if (height.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("h", height.Value));
+ if (constraint.HasValue)
+ attrs.Add(new("constraint", constraint.Value ? "on" : "off", true));
+
+ AdvancedTextNode node = new(this, CreateNodeId(), attrs.ToArray());
+ Nodes.Add(node);
+ return node;
}
public string Render()
@@ -67,9 +155,27 @@ public string Render()
Interaction.RenderTo(builder);
Styling.RenderTo(builder);
+ EdgeStyling.RenderTo(builder);
return builder.ToString();
}
private string CreateNodeId() => "id" + Nodes.Count;
+
+ private void AddRelation(
+ INode from,
+ INode to,
+ Link link,
+ string text,
+ int length,
+ string? edgeId = null)
+ {
+ if (length < 1)
+ throw new ArgumentException("Link length should be more or equal 1", nameof(length));
+ if (edgeId is not null && string.IsNullOrWhiteSpace(edgeId))
+ throw new ArgumentException("Edge id should not be empty", nameof(edgeId));
+
+ Relation relation = new(from, to, link, text, length, edgeId);
+ Relations.Add(relation);
+ }
}
diff --git a/src/FluentMermaid/Flowchart/Nodes/SubGraphNode.cs b/src/FluentMermaid/Flowchart/Nodes/SubGraphNode.cs
index 493881b..1a7df94 100644
--- a/src/FluentMermaid/Flowchart/Nodes/SubGraphNode.cs
+++ b/src/FluentMermaid/Flowchart/Nodes/SubGraphNode.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using FluentMermaid.Enums;
using FluentMermaid.Extensions;
using FluentMermaid.Flowchart.Enum;
@@ -6,7 +6,7 @@
namespace FluentMermaid.Flowchart.Nodes;
-internal record SubGraphNode : ISubGraph
+internal record SubGraphNode : IAdvancedSubGraph
{
public SubGraphNode(
string id,
@@ -24,9 +24,9 @@ public SubGraphNode(
public Orientation Orientation { get; }
- private HashSet Nodes { get; } = new();
+ private List Nodes { get; } = new();
- private HashSet Relations { get; } = new();
+ private List Relations { get; } = new();
public INode TextNode(string content, Shape shape)
{
@@ -42,6 +42,20 @@ public ISubGraph SubGraph(string title, Orientation orientation)
return subgraph;
}
+ public IAdvancedSubGraph AdvancedSubGraph(string title, Orientation orientation)
+ {
+ var subgraph = new SubGraphNode(CreateNodeId(), title, orientation);
+ Nodes.Add(subgraph);
+ return subgraph;
+ }
+
+ public IAdvancedSubGraph AdvancedSubGraph(string id, string title, Orientation orientation)
+ {
+ var subgraph = new SubGraphNode(id, title, orientation);
+ Nodes.Add(subgraph);
+ return subgraph;
+ }
+
public void RenderTo(StringBuilder builder)
{
builder
@@ -68,11 +82,87 @@ public void RenderTo(StringBuilder builder)
private string CreateNodeId() => Id + "Sub" + Nodes.Count;
public void Link(INode @from, INode to, Link link, string text, int length = 1)
+ => AddRelation(@from, to, link, text, length);
+
+ public void Link(INode from, INode to, string edgeId, Link link, string text, int length = 1)
+ => AddRelation(from, to, link, text, length, edgeId);
+
+ public INode TextNode(string content, string shapeAlias)
+ {
+ if (string.IsNullOrWhiteSpace(shapeAlias))
+ throw new ArgumentException("Shape alias should not be null or empty", nameof(shapeAlias));
+
+ AdvancedTextNode textNode = new(
+ this,
+ CreateNodeId(),
+ new AdvancedTextNode.NodeAttribute("shape", shapeAlias),
+ new AdvancedTextNode.NodeAttribute("label", content, true));
+ Nodes.Add(textNode);
+ return textNode;
+ }
+
+ public INode IconNode(string icon, string? label = null, string? form = null, string? position = null, int? height = null)
+ {
+ if (string.IsNullOrWhiteSpace(icon))
+ throw new ArgumentException("Icon should not be null or empty", nameof(icon));
+
+ var attrs = new List
+ {
+ new("icon", icon, true)
+ };
+ if (!string.IsNullOrWhiteSpace(form))
+ attrs.Add(new("form", form!, true));
+ if (!string.IsNullOrWhiteSpace(label))
+ attrs.Add(new("label", label!, true));
+ if (!string.IsNullOrWhiteSpace(position))
+ attrs.Add(new("pos", position!, true));
+ if (height.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("h", height.Value));
+
+ AdvancedTextNode node = new(this, CreateNodeId(), attrs.ToArray());
+ Nodes.Add(node);
+ return node;
+ }
+
+ public INode ImageNode(
+ Uri imageUrl,
+ string? label = null,
+ string? position = null,
+ int? width = null,
+ int? height = null,
+ bool? constraint = null)
+ {
+ if (imageUrl is null)
+ throw new ArgumentNullException(nameof(imageUrl));
+
+ var attrs = new List
+ {
+ new("img", imageUrl.ToString(), true)
+ };
+ if (!string.IsNullOrWhiteSpace(label))
+ attrs.Add(new("label", label!, true));
+ if (!string.IsNullOrWhiteSpace(position))
+ attrs.Add(new("pos", position!, true));
+ if (width.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("w", width.Value));
+ if (height.HasValue)
+ attrs.Add(AdvancedTextNode.NodeAttribute.FromInt("h", height.Value));
+ if (constraint.HasValue)
+ attrs.Add(new("constraint", constraint.Value ? "on" : "off", true));
+
+ AdvancedTextNode node = new(this, CreateNodeId(), attrs.ToArray());
+ Nodes.Add(node);
+ return node;
+ }
+
+ private void AddRelation(INode from, INode to, Link link, string text, int length, string? edgeId = null)
{
if (length < 1)
throw new ArgumentException("Link length should be more or equal 1", nameof(length));
+ if (edgeId is not null && string.IsNullOrWhiteSpace(edgeId))
+ throw new ArgumentException("Edge id should not be empty", nameof(edgeId));
- Relation relation = new(from, to, link, text, length);
+ Relation relation = new(from, to, link, text, length, edgeId);
Relations.Add(relation);
}
}
\ No newline at end of file
diff --git a/src/FluentMermaid/Flowchart/Relation.cs b/src/FluentMermaid/Flowchart/Relation.cs
index 61c17d3..4da3b9b 100644
--- a/src/FluentMermaid/Flowchart/Relation.cs
+++ b/src/FluentMermaid/Flowchart/Relation.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using FluentMermaid.Extensions;
using FluentMermaid.Flowchart.Enum;
using FluentMermaid.Flowchart.Extensions;
@@ -12,13 +12,15 @@ internal Relation(
INode to,
Link link,
string text,
- int length)
+ int length,
+ string? edgeId = null)
{
From = @from;
To = to;
Text = text;
Length = length;
Link = link;
+ EdgeId = edgeId;
}
public INode From { get; }
@@ -31,12 +33,17 @@ internal Relation(
public int Length { get; }
+ public string? EdgeId { get; }
+
public void RenderTo(StringBuilder builder)
{
builder
.Append(From.Id)
.Append(' ');
+ if (!string.IsNullOrWhiteSpace(EdgeId))
+ builder.Append(EdgeId).Append('@');
+
Link.RenderTo(builder, Length);
builder.Append(' ');
diff --git a/src/FluentMermaidLibrary.sln b/src/FluentMermaidLibrary.sln
index 6be5685..6352d98 100644
--- a/src/FluentMermaidLibrary.sln
+++ b/src/FluentMermaidLibrary.sln
@@ -2,15 +2,44 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMermaid", "FluentMermaid\FluentMermaid.csproj", "{557AD2F1-1C76-46F2-919E-D8AB7D599099}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMermaid.Tests", "..\tests\FluentMermaid.Tests\FluentMermaid.Tests.csproj", "{D60C8DD9-CEF6-424A-973F-71AAAB770A0B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|x64.Build.0 = Debug|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Debug|x86.Build.0 = Debug|Any CPU
{557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|Any CPU.ActiveCfg = Release|Any CPU
{557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|Any CPU.Build.0 = Release|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|x64.ActiveCfg = Release|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|x64.Build.0 = Release|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|x86.ActiveCfg = Release|Any CPU
+ {557AD2F1-1C76-46F2-919E-D8AB7D599099}.Release|x86.Build.0 = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|x64.Build.0 = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Debug|x86.Build.0 = Debug|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|x64.ActiveCfg = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|x64.Build.0 = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|x86.ActiveCfg = Release|Any CPU
+ {D60C8DD9-CEF6-424A-973F-71AAAB770A0B}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/tests/FluentMermaid.Tests/FlowchartRenderingTests.cs b/tests/FluentMermaid.Tests/FlowchartRenderingTests.cs
new file mode 100644
index 0000000..5bdd550
--- /dev/null
+++ b/tests/FluentMermaid.Tests/FlowchartRenderingTests.cs
@@ -0,0 +1,90 @@
+using FluentMermaid.Enums;
+using FluentMermaid.Flowchart;
+using FluentMermaid.Flowchart.Enum;
+
+namespace FluentMermaid.Tests;
+
+public class FlowchartRenderingTests
+{
+ [Fact]
+ public void LinkRendering_UsesCurrentMermaidLengthRules()
+ {
+ var chart = FlowChart.Create(Orientation.LeftToRight);
+ var start = chart.TextNode("Start", Shape.Rectangle);
+ var end = chart.TextNode("End", Shape.Rectangle);
+
+ chart.Link(start, end, Link.Thick, "", 1);
+ chart.Link(start, end, Link.Dotted, "", 2);
+
+ string rendered = Normalize(chart.Render());
+
+ Assert.Contains("id0 ==> id1", rendered);
+ Assert.Contains("id0 -..- id1", rendered);
+ }
+
+ [Fact]
+ public void LinkRendering_SupportsInvisibleAndDottedArrowLinks()
+ {
+ var chart = FlowChart.Create(Orientation.LeftToRight);
+ var a = chart.TextNode("A", Shape.Rectangle);
+ var b = chart.TextNode("B", Shape.Rectangle);
+
+ chart.Link(a, b, Link.Invisible, "", 1);
+ chart.Link(a, b, Link.DottedArrow, "", 1);
+
+ string rendered = Normalize(chart.Render());
+
+ Assert.Contains("id0 ~~~ id1", rendered);
+ Assert.Contains("id0 -.-> id1", rendered);
+ }
+
+ [Fact]
+ public void AdvancedApi_RendersExpandedShapeSyntax()
+ {
+ var chart = FlowChart.CreateAdvanced(Orientation.LeftToRight);
+ chart.TextNode("Manual input", AdvancedShape.SlopedRectangle);
+
+ string rendered = Normalize(chart.Render());
+
+ Assert.Contains("id0@{ shape: sl-rect, label: \"Manual input\" }", rendered);
+ }
+
+ [Fact]
+ public void AdvancedApi_RendersIconAndImageNodes()
+ {
+ var chart = FlowChart.CreateAdvanced(Orientation.LeftToRight);
+ chart.IconNode("fa:user", "User Icon", "square", "t", 60);
+ chart.ImageNode(new Uri("https://example.com/image.png"), "Image Label", "t", 60, 60, false);
+
+ string rendered = Normalize(chart.Render());
+
+ Assert.Contains("id0@{ icon: \"fa:user\", form: \"square\", label: \"User Icon\", pos: \"t\", h: 60 }", rendered);
+ Assert.Contains("id1@{ img: \"https://example.com/image.png\", label: \"Image Label\", pos: \"t\", w: 60, h: 60, constraint: \"off\" }", rendered);
+ }
+
+ [Fact]
+ public void AdvancedApi_RendersEdgeIdAndEdgeProperties()
+ {
+ var chart = FlowChart.CreateAdvanced(Orientation.LeftToRight);
+ var a = chart.TextNode("A", Shape.Rectangle);
+ var b = chart.TextNode("B", Shape.Rectangle);
+
+ chart.Link(a, b, "e1", Link.Arrow, "", 1);
+ chart.EdgeStyling.SetAnimated("e1");
+ chart.EdgeStyling.SetAnimation("e1", EdgeAnimationSpeed.Fast);
+ chart.EdgeStyling.SetCurve("e1", EdgeCurve.Linear);
+ chart.EdgeStyling.SetClass("e1", "animate");
+ chart.EdgeStyling.LinkStyle(0, "stroke:#ff3");
+ chart.EdgeStyling.LinkStyleDefault("color:blue");
+
+ string rendered = Normalize(chart.Render());
+
+ Assert.Contains("id0 e1@--> id1", rendered);
+ Assert.Contains("e1@{ animate: true, animation: fast, curve: linear }", rendered);
+ Assert.Contains("class e1 animate", rendered);
+ Assert.Contains("linkStyle 0 stroke:#ff3;", rendered);
+ Assert.Contains("linkStyle default color:blue;", rendered);
+ }
+
+ private static string Normalize(string input) => input.Replace("\r\n", "\n");
+}
diff --git a/tests/FluentMermaid.Tests/FluentMermaid.Tests.csproj b/tests/FluentMermaid.Tests/FluentMermaid.Tests.csproj
new file mode 100644
index 0000000..829cf53
--- /dev/null
+++ b/tests/FluentMermaid.Tests/FluentMermaid.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/tests/FluentMermaid.Tests/GlobalUsings.cs b/tests/FluentMermaid.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/tests/FluentMermaid.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file