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"); +}