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
49 changes: 48 additions & 1 deletion src/DiagramForge/Layout/DefaultLayoutEngine.Sequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,53 @@ private static void LayoutSequenceDiagram(
if (contentRight > maxNodeRight + extraRight)
extraRight = contentRight - maxNodeRight;
}
diagram.Metadata["sequence:canvasWidth"] = maxNodeRight + extraRight + pad;
double canvasWidth = maxNodeRight + extraRight + pad;
diagram.Metadata["sequence:canvasWidth"] = canvasWidth;

// Position rect-band groups now that message Y coordinates are final.
foreach (var group in diagram.Groups)
{
if (!group.Metadata.TryGetValue("sequence:rectGroup", out var isRectObj) || isRectObj is not true)
continue;

if (!group.Metadata.TryGetValue("sequence:rectStartIndex", out var startIdxObj)
|| !group.Metadata.TryGetValue("sequence:rectEndIndex", out var endIdxObj))
continue;

int startIdx = Convert.ToInt32(startIdxObj, System.Globalization.CultureInfo.InvariantCulture);
int endIdx = Convert.ToInt32(endIdxObj, System.Globalization.CultureInfo.InvariantCulture);

double halfRow = messageRowHeight / 2;
double bandTop = double.MaxValue;
double bandBottom = double.MinValue;

foreach (var e in orderedEdges)
{
if (!TryGetMetadataInt(e.Metadata, "sequence:messageIndex", out var msgIdx))
continue;
if (msgIdx < startIdx || msgIdx > endIdx)
continue;
if (!e.Metadata.TryGetValue("sequence:messageY", out var yObj))
continue;

double y = Convert.ToDouble(yObj, System.Globalization.CultureInfo.InvariantCulture);
bool isSelf = e.Metadata.ContainsKey("sequence:selfMessage");
double rowH = isSelf ? messageRowHeight * 2 : messageRowHeight;

if (y < bandTop) bandTop = y;
// Use y + rowH - halfRow so consecutive bands meet at a shared
// midpoint rather than overlapping.
double rowBottom = y + rowH - halfRow;
if (rowBottom > bandBottom) bandBottom = rowBottom;
}

if (bandTop == double.MaxValue)
continue; // no enclosed messages found

group.X = 0;
group.Y = bandTop - halfRow;
group.Width = canvasWidth;
group.Height = bandBottom - bandTop + halfRow;
}
}
}
3 changes: 3 additions & 0 deletions src/DiagramForge/Models/Group.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@ public Group(string id, string labelText)

/// <summary>Layout size computed by the layout engine.</summary>
public double Height { get; set; }

/// <summary>Arbitrary key-value metadata attached to this group (e.g., parser-specific annotations).</summary>
public Dictionary<string, object> Metadata { get; } = new(StringComparer.Ordinal);
}
57 changes: 57 additions & 0 deletions src/DiagramForge/Parsers/Mermaid/MermaidSequenceParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public Diagram Parse(MermaidDocument document)
bool autonumber = false;
int autonumberIndex = 1;

// Stack for open rect blocks: each entry holds (group, startMessageIndex).
var openRects = new Stack<(Group Group, int StartIndex)>();
int rectIndex = 0;

Node GetOrCreateParticipant(string id)
{
if (!participants.TryGetValue(id, out var node))
Expand Down Expand Up @@ -98,6 +102,35 @@ Node GetOrCreateParticipant(string id)
continue;
}

// rect rgb(...) / rect rgba(...) — colored background band
if (line.StartsWith("rect ", StringComparison.OrdinalIgnoreCase))
{
var colorSpec = line["rect ".Length..].Trim();
if (TryParseRectColor(colorSpec, out var normalizedColor))
{
var groupId = $"sequence:rect:{rectIndex++:D4}";
var group = new Group(groupId, string.Empty);
group.FillColor = normalizedColor;
group.Metadata["sequence:rectGroup"] = true;
openRects.Push((group, messageIndex));
}
continue;
}

// end — closes the innermost open rect block
if (string.Equals(line, "end", StringComparison.OrdinalIgnoreCase) && openRects.Count > 0)
{
var (group, startIdx) = openRects.Pop();
int endIdx = messageIndex - 1;
if (endIdx >= startIdx)
{
group.Metadata["sequence:rectStartIndex"] = startIdx;
group.Metadata["sequence:rectEndIndex"] = endIdx;
builder.AddGroup(group);
}
continue;
}

// Message line: SourceId->>TargetId: label text
if (TryParseMessage(line, out var srcId, out var tgtId, out var msgLabel,
out var lineStyle, out var arrowHead))
Expand Down Expand Up @@ -128,6 +161,30 @@ Node GetOrCreateParticipant(string id)
return diagram;
}

/// <summary>
/// Validates and normalises a rect color specification of the form
/// <c>rgb(R,G,B)</c> or <c>rgba(R,G,B,A)</c>.
/// Returns <see langword="false"/> when the input does not match either form.
/// Only the structural prefix (<c>rgb(</c> / <c>rgba(</c>) and closing
/// parenthesis are validated; individual component values are passed through as-is.
/// </summary>
private static bool TryParseRectColor(string colorSpec, out string normalizedColor)
{
normalizedColor = string.Empty;
var trimmed = colorSpec.Trim();

bool isRgb = trimmed.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase);
bool isRgba = trimmed.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase);
if (!isRgb && !isRgba)
return false;

if (!trimmed.EndsWith(")"))
return false;

normalizedColor = trimmed;
return true;
}

/// <summary>
/// Attempts to parse a sequence message line of the form
/// <c>SourceId{op}TargetId: label</c> where {op} is one of the
Expand Down
26 changes: 26 additions & 0 deletions src/DiagramForge/Rendering/SvgStructureWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,14 @@ private static string BuildSelfMessageLoopPath(

internal static void AppendGroup(StringBuilder sb, Group group, Theme theme, int groupIndex)
{
// Sequence rect bands are rendered as simple semi-transparent colored rectangles
// spanning the full diagram width — no gradient, no border, no label badge.
if (group.Metadata.TryGetValue("sequence:rectGroup", out var isRectObj) && isRectObj is true)
{
AppendSequenceRectBand(sb, group);
return;
}

string baseFill = group.FillColor ?? theme.GroupFillColor;
string baseStroke = group.StrokeColor ?? theme.GroupStrokeColor;
SvgRenderSupport.AppendGradientDefs(sb, " ", $"group-{groupIndex}", baseFill, baseStroke, theme, out string fill, out string stroke);
Expand All @@ -534,6 +542,24 @@ internal static void AppendGroup(StringBuilder sb, Group group, Theme theme, int
}
}

/// <summary>
/// Renders a sequence-diagram rect band as a simple semi-transparent colored
/// rectangle spanning the full canvas width. No border, no label badge.
/// </summary>
private static void AppendSequenceRectBand(StringBuilder sb, Group group)
{
string colorSpec = group.FillColor ?? "rgb(128,128,128)";
string fill = SvgRenderSupport.Escape(colorSpec);

// For rgb() (no embedded alpha) add a fill-opacity so the band is
// semi-transparent. For rgba() the caller-supplied alpha takes effect
// automatically via the browser's CSS color parsing.
bool hasEmbeddedAlpha = colorSpec.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase);
string opacityAttr = hasEmbeddedAlpha ? string.Empty : " fill-opacity=\"0.2\"";

sb.AppendLine($""" <rect x="{SvgRenderSupport.F(group.X)}" y="{SvgRenderSupport.F(group.Y)}" width="{SvgRenderSupport.F(group.Width)}" height="{SvgRenderSupport.F(group.Height)}" fill="{fill}"{opacityAttr}/>""");
}

private static bool TryBuildCycleArcPath(Edge edge, Node source, Node target, out string pathData, out double labelX, out double labelY)
{
pathData = string.Empty;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions tests/DiagramForge.E2ETests/Fixtures/mermaid-sequence-rect.input
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
sequenceDiagram
participant A as Alice
participant B as Bob
participant C as Carol
A->>B: Initialise
rect rgb(0,128,255)
B->>C: Query
C-->>B: Result
end
rect rgba(255,128,0,0.25)
B->>A: Notify
A-->>B: Ack
end
A->>B: Done
Loading
Loading