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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 59 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ company.Add(new D2Shape("deepmind", "DeepMind", Shape.Rectangle));

company.Icon = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png";

var connection = new D2Connection(company.Name, umbrella.Name, Direction.TO, "BELONGS_TO");
var connection = new D2Connection(company.Name, umbrella.Name, Direction.To, "BELONGS_TO");

var diagram = new D2Diagram(new[] { umbrella, company }, new[] { connection });

Expand Down Expand Up @@ -56,17 +56,71 @@ google -> alphabet: BELONGS_TO
![Diagram](docs/assets/img/diagram.png)

# Documentation

## Typed diagrams

Special D2 shapes have typed helpers while still participating in the ordered
`D2Statement` model:

```csharp
var users = new D2SqlTable("users", "Users")
{
new D2SqlColumn("id", "int", D2SqlConstraint.PrimaryKey),
new D2SqlColumn("email", "string", D2SqlConstraint.Unique),
};

var service = new D2Class("user_service", "User service")
{
new D2ClassField("repository", "Repository", D2Visibility.Private),
new D2ClassMethod(
"find",
"User",
D2Visibility.Public,
new D2ClassParameter("id", "int")),
};

var request = new D2SequenceDiagram("request", "Find user")
.AddParticipant("client", "Client")
.AddParticipant("service", "Service")
.AddMessage("client", "service", "find(42)")
.AddMessage("service", "client", "User", Direction.From);

var diagram = new D2Diagram(new D2Statement[] { users, service, request });
```

All style arguments are optional and named arguments keep declarations compact:

```csharp
var api = new D2Shape(
"api",
Shape: Shape.Rectangle,
Style: new D2Style(
Fill: "#f4a261",
BorderRadius: 8,
Font: D2Font.Mono,
Animated: true))
{
Link = "https://example.com/docs#api",
Tooltip = "Open API docs",
Width = 240,
Height = 120,
};
```

## Supported
- [x] Shapes (nodes)
- [x] Connections (edges)
- [x] Styles
- [x] Full documented style catalog
- [x] Containers (nodes/links in nodes)
- [x] Arrow directions
- [x] Markdown / latex / block strings / code in shapes
- [x] Shape icons
- [ ] SQL table shapes
- [ ] Class shapes
- [ ] Comments
- [x] Shape dimensions, links, and tooltips
- [x] Connection icons, styles, links, and tooltips
- [x] Typed SQL table shapes and constraints
- [x] Typed UML classes, members, parameters, and visibility
- [x] Ordered sequence-diagram helpers
- [x] Comments, root properties, composition boards, and raw escape hatches

# Inspiration & Thanks
- [Kreshnik/d2lang-js](https://github.com/Kreshnik/d2lang-js)
Expand Down
4 changes: 2 additions & 2 deletions example/cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

company.Icon = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png";

var connection = new D2Connection(company.Name, umbrella.Name, Direction.TO, "BELONGS_TO");
var connection = new D2Connection(company.Name, umbrella.Name, Direction.To, "BELONGS_TO");

var diagram = new D2Diagram(new[] { umbrella, company }, new[] { connection });

Console.WriteLine(diagram.ToString());
Console.WriteLine(diagram.ToString());
198 changes: 198 additions & 0 deletions src/D2Class.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System.Collections;

namespace d2;

/// <summary>UML visibility prefixes supported by D2 class members.</summary>
public enum D2Visibility
{
/// <summary>No explicit prefix; D2 treats this as public.</summary>
Default,
/// <summary>Explicit public visibility (<c>+</c>).</summary>
Public,
/// <summary>Private visibility (<c>-</c>).</summary>
Private,
/// <summary>Protected visibility (<c>#</c>).</summary>
Protected,
}

/// <summary>A typed parameter displayed in a UML class method signature.</summary>
public sealed record D2ClassParameter
{
/// <summary>The parameter name.</summary>
public string Name { get; }
/// <summary>The parameter type.</summary>
public string Type { get; }

/// <summary>Creates a class method parameter.</summary>
public D2ClassParameter(string name, string type)
{
_ = D2Writer.Identifier(name);
if (string.IsNullOrWhiteSpace(type))
{
throw new ArgumentException("A class parameter type cannot be null, empty, or whitespace.", nameof(type));
}

Name = name;
Type = type;
}

internal string Display() => $"{Name} {Type}";
}

/// <summary>A field or method in a typed <see cref="D2Class"/>.</summary>
public abstract class D2ClassMember
{
/// <summary>The UML visibility of the member.</summary>
public D2Visibility Visibility { get; }

private protected D2ClassMember(D2Visibility visibility)
{
if (!Enum.IsDefined(typeof(D2Visibility), visibility))
{
throw new ArgumentOutOfRangeException(nameof(visibility), visibility, "Unknown UML visibility.");
}
Visibility = visibility;
}

private protected abstract string Signature { get; }

private protected abstract string? Result { get; }

internal IEnumerable<string> Lines()
{
var displayKey = VisibilityPrefix(Visibility) + Signature;
var key = Visibility == D2Visibility.Default && this is D2ClassField
? D2Writer.ObjectMemberIdentifier(displayKey)
: D2Writer.Identifier(displayKey);
return Result is null
? new[] { key }
: new[] { $"{key}: {D2Writer.String(Result)}" };
}

/// <inheritdoc />
public override string ToString() => string.Join(Environment.NewLine, Lines());

private static string VisibilityPrefix(D2Visibility visibility) => visibility switch
{
D2Visibility.Default => string.Empty,
D2Visibility.Public => "+",
D2Visibility.Private => "-",
D2Visibility.Protected => "#",
_ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, "Unknown UML visibility."),
};
}

/// <summary>A typed field in a D2 UML class.</summary>
public sealed class D2ClassField : D2ClassMember
{
/// <summary>The field name.</summary>
public string Name { get; }
/// <summary>The optional field type.</summary>
public string? Type { get; }

/// <summary>Creates a class field.</summary>
public D2ClassField(string name, string? type = null, D2Visibility visibility = D2Visibility.Default)
: base(visibility)
{
_ = D2Writer.Identifier(name);
Name = name;
Type = type;
}

private protected override string Signature => Name;
private protected override string? Result => Type;
}

/// <summary>A typed method in a D2 UML class.</summary>
public sealed class D2ClassMethod : D2ClassMember
{
/// <summary>The method name.</summary>
public string Name { get; }
/// <summary>The optional return type; <see langword="null"/> means void.</summary>
public string? ReturnType { get; }
/// <summary>The method parameters.</summary>
public IReadOnlyList<D2ClassParameter> Parameters { get; }

/// <summary>Creates a class method.</summary>
public D2ClassMethod(
string name,
string? returnType = null,
D2Visibility visibility = D2Visibility.Default,
params D2ClassParameter[] parameters)
: base(visibility)
{
_ = D2Writer.Identifier(name);
if (parameters is null) throw new ArgumentNullException(nameof(parameters));
if (parameters.Any(parameter => parameter is null))
{
throw new ArgumentException("A class method cannot contain a null parameter.", nameof(parameters));
}

Name = name;
ReturnType = returnType;
Parameters = parameters.ToList();
}

private protected override string Signature
=> $"{Name}({string.Join(", ", Parameters.Select(parameter => parameter.Display()))})";

private protected override string? Result => ReturnType;
}

/// <summary>A typed D2 UML <c>class</c> shape.</summary>
public sealed record D2Class : D2Statement, IEnumerable<D2ClassMember>
{
private readonly List<D2ClassMember> _members = new();

/// <summary>The class key.</summary>
public string Name { get; }
/// <summary>An optional displayed class label.</summary>
public string? Label { get; set; }
/// <summary>Typed styles applied to the class.</summary>
public D2Style? Style { get; set; }
/// <summary>An optional click destination.</summary>
public string? Link { get; set; }
/// <summary>Optional hover text.</summary>
public string? Tooltip { get; set; }
/// <summary>The class's ordered members.</summary>
public IReadOnlyList<D2ClassMember> Members => _members;

/// <summary>Creates an empty UML class.</summary>
public D2Class(string name, string? label = null)
{
_ = D2Writer.Reference(name);
Name = name;
Label = label;
}

/// <summary>Adds a typed member. Supports collection initializer syntax.</summary>
public void Add(D2ClassMember member)
{
if (member is null) throw new ArgumentNullException(nameof(member));
_members.Add(member);
}

internal override IEnumerable<string> Lines()
{
var shape = new D2Shape(Name, Label, Shape.Class, Style)
{
Link = Link,
Tooltip = Tooltip,
};
foreach (var member in _members) shape.Add(new MemberStatement(member));
return shape.Lines();
}

/// <inheritdoc />
public override string ToString() => string.Join(Environment.NewLine, Lines());

/// <inheritdoc />
public IEnumerator<D2ClassMember> GetEnumerator() => _members.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private sealed record MemberStatement(D2ClassMember Member) : D2Statement
{
internal override IEnumerable<string> Lines() => Member.Lines();
}
}
37 changes: 35 additions & 2 deletions src/D2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ public record class D2Connection(

public IReadOnlyList<D2Statement> Statements => _statements;

/// <summary>An optional icon URL displayed on this connection.</summary>
public string? Icon { get; set; }

/// <summary>An optional destination opened when this connection is clicked.</summary>
public string? Link { get; set; }

/// <summary>Optional text shown when this connection is hovered.</summary>
public string? Tooltip { get; set; }

/// <summary>Optional typed styles for this connection.</summary>
public D2Style? Style { get; set; }

public void Add(D2Property property) => Add((D2Statement)property);

public void Add(D2Statement statement)
Expand All @@ -34,14 +46,35 @@ internal override IEnumerable<string> Lines()
@base += $": {D2Writer.String(Label!)}";
}

if (_statements.Count == 0)
var properties = _statements.SelectMany(statement => statement.Lines()).ToList();
if (Icon is not null)
{
properties.Add($"icon: {D2Writer.String(Icon)}");
}

if (Link is not null)
{
properties.Add($"link: {D2Writer.String(Link)}");
}

if (Tooltip is not null)
{
properties.Add($"tooltip: {D2Writer.String(Tooltip)}");
}

if (Style is not null)
{
properties.AddRange(Style.Lines());
}

if (properties.Count == 0)
{
return new[] { @base };
}

var openingLine = hasLabel ? $"{@base} {{" : $"{@base}: {{";
return new[] { openingLine }
.Concat(D2Writer.Indent(_statements.SelectMany(statement => statement.Lines())))
.Concat(D2Writer.Indent(properties))
.Append("}");
}

Expand Down
Loading