From 566cdf2c416f8e7fb658fd2297d8930e3cf01acf Mon Sep 17 00:00:00 2001 From: wowbios Date: Thu, 5 Mar 2026 20:03:44 +0300 Subject: [PATCH] Add advanced ClassDiagram API for Mermaid v11 Keep the legacy ClassDiagram API behavior unchanged while adding an advanced entrypoint with namespace, notes, and classDef support plus focused rendering fixes and coverage tests. Made-with: Cursor --- README.md | 28 ++++ .../ClassDiagram/ClassDiagram.cs | 5 +- .../ClassDiagram/ClassDiagramRootAdvanced.cs | 146 ++++++++++++++++++ .../VisibilityAdvancedExtensions.cs | 24 +++ .../Interfaces/IClassDiagramAdvanced.cs | 16 ++ .../ClassDiagram/Nodes/CallbackAdvanced.cs | 47 ++++++ .../ClassDiagram/Nodes/ClassDefNode.cs | 35 +++++ .../Nodes/ClassMemberFunctionNodeAdvanced.cs | 77 +++++++++ .../Nodes/ClassMemberPropertyNodeAdvanced.cs | 53 +++++++ .../ClassDiagram/Nodes/ClassNodeAdvanced.cs | 126 +++++++++++++++ .../ClassDiagram/Nodes/CssClassNode.cs | 35 +++++ .../Nodes/IClassDiagramStatement.cs | 7 + .../ClassDiagram/Nodes/LinkAdvanced.cs | 42 +++++ .../ClassDiagram/Nodes/NamespaceNode.cs | 41 +++++ .../ClassDiagram/Nodes/NoteNode.cs | 42 +++++ .../ClassDiagramRenderingTests.cs | 74 +++++++++ 16 files changed, 797 insertions(+), 1 deletion(-) create mode 100644 src/FluentMermaid/ClassDiagram/ClassDiagramRootAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Extensions/VisibilityAdvancedExtensions.cs create mode 100644 src/FluentMermaid/ClassDiagram/Interfaces/IClassDiagramAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/CallbackAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/ClassDefNode.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/ClassMemberFunctionNodeAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/ClassMemberPropertyNodeAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/ClassNodeAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/CssClassNode.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/IClassDiagramStatement.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/LinkAdvanced.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/NamespaceNode.cs create mode 100644 src/FluentMermaid/ClassDiagram/Nodes/NoteNode.cs create mode 100644 tests/FluentMermaid.Tests/ClassDiagramRenderingTests.cs diff --git a/README.md b/README.md index 7a26e8f..23cb62e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,34 @@ sequence.Destroy(db); var mermaid = sequence.Build(); ``` +## Class API status (Mermaid v11) + +- Backward compatible API: existing `ClassDiagram.Create(...)` and current output are preserved. +- New additive API: `ClassDiagram.CreateAdvanced(...)`. +- Advanced additions: `namespace`, `note`, `note for`, `classDef`, `cssClass`. +- Advanced rendering fixes: visibility/classifier output for members, method argument separators, consistent class ID rendering for `link` and `callback`. + +### Quick class example + +```csharp +using FluentMermaid.ClassDiagram; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Nodes; +using FluentMermaid.Enums; + +var diagram = ClassDiagram.CreateAdvanced(Orientation.LeftToRight); +var order = diagram.AddClassInNamespace("Domain", new TypeName("Order", null), "entity", "domainClass"); +order.AddProperty("id", new TypeName("Guid", null), Visibility.Public); +order.AddFunction("Validate", new TypeName("bool", null), Visibility.Public, new FunctionArgument("source", new TypeName("string", null))); + +diagram.AddNote("Generated by FluentMermaid"); +diagram.AddNoteFor(order, "Aggregate root"); +diagram.AddClassDef("domainClass", "fill:#eef,stroke:#88a;"); +diagram.AddCssClass("Order", "domainClass"); + +var mermaid = diagram.Render(); +``` + # Roadmap - [x] [Flowchart](https://mermaid.js.org/syntax/flowchart.html) - [x] [Sequence diagram](https://mermaid.js.org/syntax/sequenceDiagram.html) diff --git a/src/FluentMermaid/ClassDiagram/ClassDiagram.cs b/src/FluentMermaid/ClassDiagram/ClassDiagram.cs index fece331..e34750b 100644 --- a/src/FluentMermaid/ClassDiagram/ClassDiagram.cs +++ b/src/FluentMermaid/ClassDiagram/ClassDiagram.cs @@ -1,4 +1,4 @@ -using FluentMermaid.ClassDiagram.Interfaces; +using FluentMermaid.ClassDiagram.Interfaces; using FluentMermaid.Enums; namespace FluentMermaid.ClassDiagram; @@ -7,4 +7,7 @@ public static class ClassDiagram { public static IClassDiagram Create(Orientation orientation) => new ClassDiagramRoot(orientation); + + public static IClassDiagramAdvanced CreateAdvanced(Orientation orientation) + => new ClassDiagramRootAdvanced(orientation); } \ No newline at end of file diff --git a/src/FluentMermaid/ClassDiagram/ClassDiagramRootAdvanced.cs b/src/FluentMermaid/ClassDiagram/ClassDiagramRootAdvanced.cs new file mode 100644 index 0000000..c0b1f9c --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/ClassDiagramRootAdvanced.cs @@ -0,0 +1,146 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Interfaces; +using FluentMermaid.ClassDiagram.Interfaces.ClassMembers; +using FluentMermaid.ClassDiagram.Nodes; +using FluentMermaid.Enums; +using FluentMermaid.Extensions; + +namespace FluentMermaid.ClassDiagram; + +internal class ClassDiagramRootAdvanced : IClassDiagramAdvanced +{ + private readonly List _classes = new(); + private readonly List _relations = new(); + private readonly List _statements = new(); + private readonly Dictionary> _namespaces = new(StringComparer.Ordinal); + private readonly List _namespaceOrder = new(); + + public ClassDiagramRootAdvanced(Orientation orientation) + { + Orientation = orientation; + } + + public Orientation Orientation { get; } + + public IClass AddClass(ITypeName typeName, string? annotation, string? cssClass) + { + _ = typeName ?? throw new ArgumentNullException(nameof(typeName)); + + var @class = new ClassNodeAdvanced(typeName, annotation, cssClass); + _classes.Add(@class); + return @class; + } + + public IClass AddClassInNamespace(string @namespace, ITypeName typeName, string? annotation, string? cssClass) + { + if (string.IsNullOrWhiteSpace(@namespace)) + { + throw new ArgumentException("Namespace should not be null or empty", nameof(@namespace)); + } + + var @class = (ClassNodeAdvanced)AddClass(typeName, annotation, cssClass); + + if (!_namespaces.TryGetValue(@namespace, out List? classes)) + { + classes = new List(); + _namespaces[@namespace] = classes; + _namespaceOrder.Add(@namespace); + } + + classes.Add(@class); + return @class; + } + + public IRelation Relation( + IClass from, + IClass to, + Relationship? relationshipFrom, + Cardinality? cardinalityFrom, + Relationship? relationshipTo, + Cardinality? cardinalityTo, + RelationLink relationLink, + string? label) + { + var relation = new RelationNode( + from, + to, + relationshipFrom, + cardinalityFrom, + relationLink, + cardinalityTo, + relationshipTo, + label); + _relations.Add(relation); + return relation; + } + + public IClassDiagramAdvanced AddNote(string text) + { + _statements.Add(new NoteNode(text)); + return this; + } + + public IClassDiagramAdvanced AddNoteFor(IClass @class, string text) + { + if (@class is not ClassNodeAdvanced classNode) + { + throw new ArgumentException("Note target should be created by advanced class diagram API", nameof(@class)); + } + + _statements.Add(new NoteNode(text, classNode)); + return this; + } + + public IClassDiagramAdvanced AddClassDef(string className, string styles) + { + _statements.Add(new ClassDefNode(className, styles)); + return this; + } + + public IClassDiagramAdvanced AddCssClass(string classIds, string className) + { + _statements.Add(new CssClassNode(classIds, className)); + return this; + } + + public string Render() + { + StringBuilder builder = new(); + builder.AppendLine("classDiagram"); + builder.Append("direction ").AppendLine(Orientation.Render()); + + _relations.ForEach(r => r.RenderTo(builder)); + + HashSet namespaceClasses = new(); + foreach (string namespaceName in _namespaceOrder) + { + List classes = _namespaces[namespaceName]; + new NamespaceNode(namespaceName, classes).RenderTo(builder); + foreach (ClassNodeAdvanced @class in classes) + { + namespaceClasses.Add(@class); + } + } + + foreach (ClassNodeAdvanced @class in _classes) + { + if (!namespaceClasses.Contains(@class)) + { + @class.RenderDeclarationTo(builder, null); + } + } + + foreach (IClassDiagramStatement statement in _statements) + { + statement.RenderTo(builder); + } + + foreach (ClassNodeAdvanced @class in _classes) + { + @class.RenderMetadataTo(builder); + } + + return builder.ToString(); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Extensions/VisibilityAdvancedExtensions.cs b/src/FluentMermaid/ClassDiagram/Extensions/VisibilityAdvancedExtensions.cs new file mode 100644 index 0000000..54f9202 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Extensions/VisibilityAdvancedExtensions.cs @@ -0,0 +1,24 @@ +using FluentMermaid.ClassDiagram.Enums; + +namespace FluentMermaid.ClassDiagram.Extensions; + +internal static class VisibilityAdvancedExtensions +{ + public static char RenderPrefix(this Visibility visibility) + => visibility switch + { + Visibility.Public => '+', + Visibility.Private => '-', + Visibility.Protected => '#', + Visibility.Internal => '~', + _ => ' ' + }; + + public static char RenderSuffix(this Visibility visibility) + => visibility switch + { + Visibility.Abstract => '*', + Visibility.Static => '$', + _ => ' ' + }; +} diff --git a/src/FluentMermaid/ClassDiagram/Interfaces/IClassDiagramAdvanced.cs b/src/FluentMermaid/ClassDiagram/Interfaces/IClassDiagramAdvanced.cs new file mode 100644 index 0000000..2474475 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Interfaces/IClassDiagramAdvanced.cs @@ -0,0 +1,16 @@ +using FluentMermaid.ClassDiagram.Interfaces.ClassMembers; + +namespace FluentMermaid.ClassDiagram.Interfaces; + +public interface IClassDiagramAdvanced : IClassDiagram +{ + IClass AddClassInNamespace(string @namespace, ITypeName typeName, string? annotation, string? cssClass); + + IClassDiagramAdvanced AddNote(string text); + + IClassDiagramAdvanced AddNoteFor(IClass @class, string text); + + IClassDiagramAdvanced AddClassDef(string className, string styles); + + IClassDiagramAdvanced AddCssClass(string classIds, string className); +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/CallbackAdvanced.cs b/src/FluentMermaid/ClassDiagram/Nodes/CallbackAdvanced.cs new file mode 100644 index 0000000..a33d773 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/CallbackAdvanced.cs @@ -0,0 +1,47 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Interfaces; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class CallbackAdvanced : ICallback +{ + private readonly ClassNodeAdvanced _class; + + public CallbackAdvanced(ClassNodeAdvanced @class, string function, string? tooltip) + { + if (string.IsNullOrWhiteSpace(function)) + { + throw new ArgumentException("Function name should not be null or empty", nameof(function)); + } + + _class = @class ?? throw new ArgumentNullException(nameof(@class)); + Function = function; + Tooltip = tooltip; + } + + public IClass Class => _class; + + public string Function { get; } + + public string? Tooltip { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("callback ") + .Append(_class.ClassNameId) + .Append(" \"") + .Append(Function) + .Append('"'); + + if (!string.IsNullOrWhiteSpace(Tooltip)) + { + builder + .Append(" \"") + .Append(Tooltip) + .Append('"'); + } + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/ClassDefNode.cs b/src/FluentMermaid/ClassDiagram/Nodes/ClassDefNode.cs new file mode 100644 index 0000000..229d159 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/ClassDefNode.cs @@ -0,0 +1,35 @@ +using System.Text; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class ClassDefNode : IClassDiagramStatement +{ + public ClassDefNode(string className, string styles) + { + if (string.IsNullOrWhiteSpace(className)) + { + throw new ArgumentException("ClassDef class name should not be null or empty", nameof(className)); + } + + if (string.IsNullOrWhiteSpace(styles)) + { + throw new ArgumentException("ClassDef styles should not be null or empty", nameof(styles)); + } + + ClassName = className; + Styles = styles; + } + + public string ClassName { get; } + + public string Styles { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("classDef ") + .Append(ClassName) + .Append(' ') + .AppendLine(Styles); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberFunctionNodeAdvanced.cs b/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberFunctionNodeAdvanced.cs new file mode 100644 index 0000000..29ff322 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberFunctionNodeAdvanced.cs @@ -0,0 +1,77 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Extensions; +using FluentMermaid.ClassDiagram.Interfaces.ClassMembers; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class ClassMemberFunctionNodeAdvanced : IClassMemberFunction +{ + public ClassMemberFunctionNodeAdvanced( + string function, + FunctionArgument[]? arguments, + ITypeName? returnType, + Visibility? visibility) + { + if (string.IsNullOrWhiteSpace(function)) + { + throw new ArgumentException("Function name should not be null or empty", nameof(function)); + } + + Function = function; + ReturnType = returnType; + Arguments = arguments; + Visibility = visibility; + } + + public Visibility? Visibility { get; } + + public string Function { get; } + + public FunctionArgument[]? Arguments { get; } + + public ITypeName? ReturnType { get; } + + public void RenderTo(StringBuilder builder) + { + char prefix = Visibility.HasValue ? Visibility.Value.RenderPrefix() : ' '; + char suffix = Visibility.HasValue ? Visibility.Value.RenderSuffix() : ' '; + + if (prefix != ' ') + { + builder.Append(prefix); + } + + builder + .AppendValidName(Function) + .Append('('); + + if (Arguments is { Length: > 0 }) + { + for (var i = 0; i < Arguments.Length; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + Arguments[i].RenderTo(builder); + } + } + + builder.Append(')'); + + if (ReturnType is not null) + { + builder.Append(' '); + ReturnType.RenderTo(builder); + } + + if (suffix != ' ') + { + builder.Append(suffix); + } + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberPropertyNodeAdvanced.cs b/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberPropertyNodeAdvanced.cs new file mode 100644 index 0000000..2ef12ef --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/ClassMemberPropertyNodeAdvanced.cs @@ -0,0 +1,53 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Extensions; +using FluentMermaid.ClassDiagram.Interfaces.ClassMembers; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class ClassMemberPropertyNodeAdvanced : IClassMemberProperty +{ + public ClassMemberPropertyNodeAdvanced(string name, ITypeName? type, Visibility? visibility) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Property name should not be null or empty", nameof(name)); + } + + Name = name; + Type = type; + Visibility = visibility; + } + + public Visibility? Visibility { get; } + + public string Name { get; } + + public ITypeName? Type { get; } + + public void RenderTo(StringBuilder builder) + { + char prefix = Visibility.HasValue ? Visibility.Value.RenderPrefix() : ' '; + char suffix = Visibility.HasValue ? Visibility.Value.RenderSuffix() : ' '; + + if (prefix != ' ') + { + builder.Append(prefix); + } + + if (Type is not null) + { + Type.RenderTo(builder); + builder.Append(' '); + } + + builder.AppendValidName(Name); + + if (suffix != ' ') + { + builder.Append(suffix); + } + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/ClassNodeAdvanced.cs b/src/FluentMermaid/ClassDiagram/Nodes/ClassNodeAdvanced.cs new file mode 100644 index 0000000..7137d27 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/ClassNodeAdvanced.cs @@ -0,0 +1,126 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Extensions; +using FluentMermaid.ClassDiagram.Interfaces; +using FluentMermaid.ClassDiagram.Interfaces.ClassMembers; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class ClassNodeAdvanced : IClass +{ + private readonly List _members = new(); + + public ClassNodeAdvanced(ITypeName typeName, string? annotation, string? cssClass) + { + Name = typeName ?? throw new ArgumentNullException(nameof(typeName)); + Annotation = annotation; + CssClass = cssClass; + } + + public ITypeName Name { get; } + + public string? Annotation { get; } + + public string? CssClass { get; } + + private LinkAdvanced? Link { get; set; } + + private CallbackAdvanced? Callback { get; set; } + + public IClassMemberFunction AddFunction(string name, ITypeName? returnType, Visibility? visibility, params FunctionArgument[] arguments) + { + var member = new ClassMemberFunctionNodeAdvanced(name, arguments, returnType, visibility); + _members.Add(member); + return member; + } + + public IClassMemberProperty AddProperty(string name, ITypeName? type, Visibility? visibility) + { + var member = new ClassMemberPropertyNodeAdvanced(name, type, visibility); + _members.Add(member); + return member; + } + + public ICallback SetCallback(string function, string? tooltip) + { + var callback = new CallbackAdvanced(this, function, tooltip); + Callback = callback; + return callback; + } + + public ILink SetLink(Uri url, string? tooltip) + { + var link = new LinkAdvanced(this, url, tooltip); + Link = link; + return link; + } + + public void RenderTo(StringBuilder builder) + { + RenderDeclarationTo(builder, null); + RenderMetadataTo(builder); + } + + public void RenderDeclarationTo(StringBuilder builder, string? indent) + { + if (!string.IsNullOrEmpty(indent)) + { + builder.Append(indent); + } + + builder.Append("class "); + Name.RenderTo(builder); + + if (_members.Count > 0) + { + builder.AppendLine(" {"); + + foreach (IClassMember member in _members) + { + if (!string.IsNullOrEmpty(indent)) + { + builder.Append(indent); + } + + builder.Append(" "); + member.RenderTo(builder); + } + + if (!string.IsNullOrEmpty(indent)) + { + builder.Append(indent); + } + + builder.Append("}"); + } + + builder.AppendLine(); + } + + public void RenderMetadataTo(StringBuilder builder) + { + if (!string.IsNullOrWhiteSpace(Annotation)) + { + builder + .Append("<<") + .Append(Annotation) + .Append(">> "); + Name.RenderTo(builder); + builder.AppendLine(); + } + + Link?.RenderTo(builder); + Callback?.RenderTo(builder); + + if (!string.IsNullOrWhiteSpace(CssClass)) + { + builder + .Append("cssClass \"") + .Append(ClassNameId) + .Append("\" ") + .AppendLine(CssClass); + } + } + + internal string ClassNameId => Name.Name.ToValidClassName(); +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/CssClassNode.cs b/src/FluentMermaid/ClassDiagram/Nodes/CssClassNode.cs new file mode 100644 index 0000000..cefb883 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/CssClassNode.cs @@ -0,0 +1,35 @@ +using System.Text; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class CssClassNode : IClassDiagramStatement +{ + public CssClassNode(string classIds, string className) + { + if (string.IsNullOrWhiteSpace(classIds)) + { + throw new ArgumentException("Class ids should not be null or empty", nameof(classIds)); + } + + if (string.IsNullOrWhiteSpace(className)) + { + throw new ArgumentException("Class name should not be null or empty", nameof(className)); + } + + ClassIds = classIds; + ClassName = className; + } + + public string ClassIds { get; } + + public string ClassName { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("cssClass \"") + .Append(ClassIds) + .Append("\" ") + .AppendLine(ClassName); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/IClassDiagramStatement.cs b/src/FluentMermaid/ClassDiagram/Nodes/IClassDiagramStatement.cs new file mode 100644 index 0000000..119084d --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/IClassDiagramStatement.cs @@ -0,0 +1,7 @@ +using System.Text; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal interface IClassDiagramStatement : IRenderTo +{ +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/LinkAdvanced.cs b/src/FluentMermaid/ClassDiagram/Nodes/LinkAdvanced.cs new file mode 100644 index 0000000..f3259fd --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/LinkAdvanced.cs @@ -0,0 +1,42 @@ +using System.Text; +using FluentMermaid.ClassDiagram.Interfaces; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class LinkAdvanced : ILink +{ + private readonly ClassNodeAdvanced _class; + + public LinkAdvanced(ClassNodeAdvanced @class, Uri url, string? tooltip) + { + _class = @class ?? throw new ArgumentNullException(nameof(@class)); + Url = url ?? throw new ArgumentNullException(nameof(url)); + Tooltip = tooltip; + } + + public IClass Class => _class; + + public Uri Url { get; } + + public string? Tooltip { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("link ") + .Append(_class.ClassNameId) + .Append(" \"") + .Append(Url) + .Append('"'); + + if (!string.IsNullOrWhiteSpace(Tooltip)) + { + builder + .Append(" \"") + .Append(Tooltip) + .Append('"'); + } + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/NamespaceNode.cs b/src/FluentMermaid/ClassDiagram/Nodes/NamespaceNode.cs new file mode 100644 index 0000000..75417e2 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/NamespaceNode.cs @@ -0,0 +1,41 @@ +using System.Text; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class NamespaceNode : IClassDiagramStatement +{ + public NamespaceNode(string name, IReadOnlyCollection classes) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Namespace name should not be null or empty", nameof(name)); + } + + Name = name; + Classes = classes ?? throw new ArgumentNullException(nameof(classes)); + } + + public string Name { get; } + + public IReadOnlyCollection Classes { get; } + + public void RenderTo(StringBuilder builder) + { + if (Classes.Count == 0) + { + return; + } + + builder + .Append("namespace ") + .Append(Name) + .AppendLine(" {"); + + foreach (ClassNodeAdvanced @class in Classes) + { + @class.RenderDeclarationTo(builder, " "); + } + + builder.AppendLine("}"); + } +} diff --git a/src/FluentMermaid/ClassDiagram/Nodes/NoteNode.cs b/src/FluentMermaid/ClassDiagram/Nodes/NoteNode.cs new file mode 100644 index 0000000..9ea2413 --- /dev/null +++ b/src/FluentMermaid/ClassDiagram/Nodes/NoteNode.cs @@ -0,0 +1,42 @@ +using System.Text; + +namespace FluentMermaid.ClassDiagram.Nodes; + +internal class NoteNode : IClassDiagramStatement +{ + private readonly ClassNodeAdvanced? _class; + + public NoteNode(string text, ClassNodeAdvanced? @class = null) + { + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException("Note text should not be null or empty", nameof(text)); + } + + Text = text; + _class = @class; + } + + public string Text { get; } + + public void RenderTo(StringBuilder builder) + { + if (_class is null) + { + builder + .Append("note \"") + .Append(Escape(Text)) + .AppendLine("\""); + return; + } + + builder + .Append("note for ") + .Append(_class.ClassNameId) + .Append(" \"") + .Append(Escape(Text)) + .AppendLine("\""); + } + + private static string Escape(string text) => text.Replace("\"", "\\\""); +} diff --git a/tests/FluentMermaid.Tests/ClassDiagramRenderingTests.cs b/tests/FluentMermaid.Tests/ClassDiagramRenderingTests.cs new file mode 100644 index 0000000..84675cf --- /dev/null +++ b/tests/FluentMermaid.Tests/ClassDiagramRenderingTests.cs @@ -0,0 +1,74 @@ +using FluentMermaid.ClassDiagram; +using FluentMermaid.ClassDiagram.Enums; +using FluentMermaid.ClassDiagram.Nodes; +using FluentMermaid.Enums; +using ClassDiagramBuilder = FluentMermaid.ClassDiagram.ClassDiagram; + +namespace FluentMermaid.Tests; + +public class ClassDiagramRenderingTests +{ + [Fact] + public void LegacyApi_SmokeRender_Works() + { + var diagram = ClassDiagramBuilder.Create(Orientation.LeftToRight); + var person = diagram.AddClass(new TypeName("Person", null), null, null); + person.AddProperty("name", new TypeName("string", null), Visibility.Public); + person.AddFunction("GetName", new TypeName("string", null), Visibility.Public); + + string rendered = Normalize(diagram.Render()); + + Assert.Contains("classDiagram", rendered); + Assert.Contains("direction LR", rendered); + Assert.Contains("class Person {", rendered); + } + + [Fact] + public void AdvancedApi_RendersNamespaceNoteAndClassDef() + { + var diagram = ClassDiagramBuilder.CreateAdvanced(Orientation.TopToBottom); + var invoice = diagram.AddClassInNamespace("Billing", new TypeName("Invoice", null), "entity", "domain"); + invoice.AddProperty("number", new TypeName("string", null), Visibility.Public); + + diagram.AddNote("General note"); + diagram.AddNoteFor(invoice, "Invoice note"); + diagram.AddClassDef("domain", "fill:#f9f,stroke:#333;"); + diagram.AddCssClass("Invoice", "domain"); + + string rendered = Normalize(diagram.Render()); + + Assert.Contains("namespace Billing {", rendered); + Assert.Contains("class Invoice {", rendered); + Assert.Contains("note \"General note\"", rendered); + Assert.Contains("note for Invoice \"Invoice note\"", rendered); + Assert.Contains("classDef domain fill:#f9f,stroke:#333;", rendered); + Assert.Contains("cssClass \"Invoice\" domain", rendered); + } + + [Fact] + public void AdvancedApi_RendersFixedVisibilityArgumentsAndIds() + { + var diagram = ClassDiagramBuilder.CreateAdvanced(Orientation.LeftToRight); + var service = diagram.AddClass(new TypeName("User.Service", null), null, null); + service.AddFunction( + "Process", + new TypeName("string", null), + Visibility.Abstract, + new FunctionArgument("id", new TypeName("int", null)), + new FunctionArgument("name", new TypeName("string", null))); + service.AddFunction("Get", null, Visibility.Public); + service.AddProperty("Version", new TypeName("string", null), Visibility.Static); + service.SetCallback("onClick", "Open"); + service.SetLink(new Uri("https://example.com"), "Go"); + + string rendered = Normalize(diagram.Render()); + + Assert.Contains("Process(int id, string name) string*", rendered); + Assert.Contains("+Get()", rendered); + Assert.Contains("string Version$", rendered); + Assert.Contains("callback User_Service \"onClick\" \"Open\"", rendered); + Assert.Contains("link User_Service \"https://example.com/\" \"Go\"", rendered); + } + + private static string Normalize(string input) => input.Replace("\r\n", "\n"); +}