Skip to content

Commit ab82c3b

Browse files
grokifyclaude
andcommitted
feat(render): update renderers to support new flow and phase features
Mermaid renderer: - Render conditions using opt blocks - Render alternatives using alt/else blocks - Render notes using note right of - Render annotations as prefixed notes - Render nested phases as colored rect blocks PlantUML renderer: - Render conditions using opt blocks - Render alternatives using alt/else blocks - Render notes using note right - Render annotations with OpenIconic icons - Render nested phases as colored box containers D2 renderer: - Render conditions as label prefixes - Render alternatives as grouped flows - Render notes as tooltips - Render annotations as separate note messages - Render nested phases as nested groups DOT renderer: - Render conditions as edge label prefixes - Render alternatives as separate colored edges - Add ShowConditions and ShowAnnotations options Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 37afe81 commit ab82c3b

4 files changed

Lines changed: 486 additions & 62 deletions

File tree

render/d2.go

Lines changed: 141 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ type D2Renderer struct {
3333

3434
// Direction sets the diagram direction (down, right, left, up).
3535
Direction string
36+
37+
// ShowNotes renders flow notes as D2 notes/labels.
38+
ShowNotes bool
39+
40+
// ShowAnnotations renders flow annotations.
41+
ShowAnnotations bool
42+
43+
// ShowConditions indicates conditional flows.
44+
ShowConditions bool
45+
46+
// ShowAlternatives renders alternative paths.
47+
ShowAlternatives bool
3648
}
3749

3850
// NewD2 creates a new D2 renderer with default options (sequence diagram).
@@ -42,6 +54,10 @@ func NewD2() *D2Renderer {
4254
Title: true,
4355
ShowDescriptions: false,
4456
Direction: "right",
57+
ShowNotes: true,
58+
ShowAnnotations: true,
59+
ShowConditions: true,
60+
ShowAlternatives: true,
4561
}
4662
}
4763

@@ -52,6 +68,10 @@ func NewD2Flow() *D2Renderer {
5268
Title: true,
5369
ShowDescriptions: false,
5470
Direction: "right",
71+
ShowNotes: true,
72+
ShowAnnotations: true,
73+
ShowConditions: true,
74+
ShowAlternatives: true,
5575
}
5676
}
5777

@@ -62,6 +82,10 @@ func NewD2Arch() *D2Renderer {
6282
Title: true,
6383
ShowDescriptions: false,
6484
Direction: "right",
85+
ShowNotes: true,
86+
ShowAnnotations: true,
87+
ShowConditions: true,
88+
ShowAlternatives: true,
6589
}
6690
}
6791

@@ -122,46 +146,40 @@ func (r *D2Renderer) renderSequence(p *pidl.Protocol) string {
122146
// Track sequence number for ordering
123147
seq := 1
124148

125-
// Track current phase for grouping
149+
// Track current phase for grouping and nesting
126150
currentPhase := ""
127-
inPhaseGroup := false
151+
phaseStack := []string{}
128152

129153
for _, f := range p.Flows {
130-
// Handle phase changes
131-
if f.Phase != "" && f.Phase != currentPhase {
132-
if inPhaseGroup {
154+
// Handle phase changes with nesting support
155+
if f.Phase != currentPhase {
156+
// Close previous phase groups
157+
for range phaseStack {
133158
sb.WriteString(" }\n\n")
134159
}
135-
phase := p.PhaseByID(f.Phase)
136-
if phase != nil {
137-
fmt.Fprintf(&sb, " %s: %s {\n", r.sanitizeID(f.Phase), phase.Name)
138-
inPhaseGroup = true
160+
phaseStack = nil
161+
162+
// Open new phase groups (including parent hierarchy)
163+
if f.Phase != "" {
164+
phase := p.PhaseByID(f.Phase)
165+
if phase != nil {
166+
phaseStack = r.openPhaseGroups(&sb, p, phase)
167+
}
139168
}
140169
currentPhase = f.Phase
141170
}
142171

143172
// Render the flow
144173
indent := " "
145-
if inPhaseGroup {
146-
indent = " "
174+
for range phaseStack {
175+
indent += " "
147176
}
148177

149-
from := r.sanitizeID(f.From)
150-
to := r.sanitizeID(f.To)
151-
label := f.DisplayLabel()
152-
153-
// Add mode annotation
154-
if ann := r.modeAnnotation(f.EffectiveMode()); ann != "" {
155-
label = fmt.Sprintf("%s (%s)", label, ann)
156-
}
157-
158-
// D2 sequence diagram message syntax
159-
arrow := r.modeToArrow(f.EffectiveMode())
160-
fmt.Fprintf(&sb, "%sseq%d: %s %s %s: %s\n", indent, seq, from, arrow, to, label)
161-
seq++
178+
seq = r.renderSequenceFlow(&sb, p, f, indent, seq)
162179
}
163180

164-
if inPhaseGroup {
181+
// Close remaining phase groups
182+
for range phaseStack {
165183
sb.WriteString(" }\n")
166184
}
167185

@@ -170,6 +188,104 @@ func (r *D2Renderer) renderSequence(p *pidl.Protocol) string {
170188
return sb.String()
171189
}
172190

191+
// openPhaseGroups opens D2 groups for a phase and its parent hierarchy, returns the stack.
192+
func (r *D2Renderer) openPhaseGroups(sb *strings.Builder, p *pidl.Protocol, phase *pidl.Phase) []string {
193+
// Build the hierarchy from root to current phase
194+
var hierarchy []*pidl.Phase
195+
current := phase
196+
for current != nil {
197+
hierarchy = append([]*pidl.Phase{current}, hierarchy...)
198+
if current.Parent == "" {
199+
break
200+
}
201+
current = p.PhaseByID(current.Parent)
202+
}
203+
204+
// Open groups from root to leaf
205+
var stack []string
206+
for i, ph := range hierarchy {
207+
indent := " "
208+
for j := 0; j < i; j++ {
209+
indent += " "
210+
}
211+
fmt.Fprintf(sb, "%s%s: %s {\n", indent, r.sanitizeID(ph.ID), ph.Name)
212+
stack = append(stack, ph.ID)
213+
}
214+
215+
return stack
216+
}
217+
218+
// renderSequenceFlow renders a single flow in a D2 sequence diagram.
219+
func (r *D2Renderer) renderSequenceFlow(sb *strings.Builder, _ *pidl.Protocol, f pidl.Flow, indent string, seq int) int {
220+
from := r.sanitizeID(f.From)
221+
to := r.sanitizeID(f.To)
222+
label := f.DisplayLabel()
223+
224+
// Add condition to label if present
225+
if r.ShowConditions && f.HasCondition() {
226+
label = fmt.Sprintf("[%s] %s", f.Condition, label)
227+
}
228+
229+
// Add mode annotation
230+
if ann := r.modeAnnotation(f.EffectiveMode()); ann != "" {
231+
label = fmt.Sprintf("%s (%s)", label, ann)
232+
}
233+
234+
// D2 sequence diagram message syntax
235+
arrow := r.modeToArrow(f.EffectiveMode())
236+
fmt.Fprintf(sb, "%sseq%d: %s %s %s: %s", indent, seq, from, arrow, to, label)
237+
238+
// Add note as tooltip if present
239+
if r.ShowNotes && f.HasNote() {
240+
fmt.Fprintf(sb, " {\n%s tooltip: %s\n%s}", indent, f.Note, indent)
241+
}
242+
243+
sb.WriteString("\n")
244+
seq++
245+
246+
// Render annotations as separate note messages
247+
if r.ShowAnnotations && f.HasAnnotations() {
248+
for _, ann := range f.Annotations {
249+
prefix := r.annotationPrefix(ann.Type)
250+
fmt.Fprintf(sb, "%snote%d: %s -> %s: %s%s\n", indent, seq, to, to, prefix, ann.Text)
251+
seq++
252+
}
253+
}
254+
255+
// Render alternatives as additional flows
256+
if r.ShowAlternatives && f.HasAlternatives() {
257+
for _, alt := range f.Alternatives {
258+
fmt.Fprintf(sb, "%salt%d: [%s] {\n", indent, seq, alt.Condition)
259+
altIndent := indent + " "
260+
for _, altFlow := range alt.Flows {
261+
seq = r.renderSequenceFlow(sb, nil, altFlow, altIndent, seq)
262+
}
263+
fmt.Fprintf(sb, "%s}\n", indent)
264+
seq++
265+
}
266+
}
267+
268+
return seq
269+
}
270+
271+
// annotationPrefix returns a visual prefix for annotation types.
272+
func (r *D2Renderer) annotationPrefix(t pidl.AnnotationType) string {
273+
switch t {
274+
case pidl.AnnotationTypeSecurity:
275+
return "⚠️ SECURITY: "
276+
case pidl.AnnotationTypePerformance:
277+
return "⏱️ PERF: "
278+
case pidl.AnnotationTypeDeprecated:
279+
return "🚫 DEPRECATED: "
280+
case pidl.AnnotationTypeWarning:
281+
return "⚠️ WARNING: "
282+
case pidl.AnnotationTypeError:
283+
return "❌ ERROR: "
284+
default:
285+
return ""
286+
}
287+
}
288+
173289
// renderFlow renders a D2 data flow diagram.
174290
func (r *D2Renderer) renderFlow(p *pidl.Protocol) string {
175291
var sb strings.Builder

render/dot.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,23 @@ type DOTRenderer struct {
2121

2222
// ShowPhases groups nodes by phase using subgraphs.
2323
ShowPhases bool
24+
25+
// ShowConditions includes condition text in edge labels.
26+
ShowConditions bool
27+
28+
// ShowAnnotations includes annotation counts in edge labels.
29+
ShowAnnotations bool
2430
}
2531

2632
// NewDOT creates a new DOT renderer with default options.
2733
func NewDOT() *DOTRenderer {
2834
return &DOTRenderer{
29-
Title: true,
30-
RankDir: "LR",
31-
MergeEdges: true,
32-
ShowPhases: false,
35+
Title: true,
36+
RankDir: "LR",
37+
MergeEdges: true,
38+
ShowPhases: false,
39+
ShowConditions: true,
40+
ShowAnnotations: false,
3341
}
3442
}
3543

@@ -129,8 +137,32 @@ func (r *DOTRenderer) renderAllEdges(sb *strings.Builder, p *pidl.Protocol) {
129137
for i, f := range p.Flows {
130138
style := r.modeToStyle(f.EffectiveMode())
131139
label := r.escapeString(f.DisplayLabel())
140+
141+
// Add condition prefix if present and enabled
142+
if r.ShowConditions && f.HasCondition() {
143+
label = fmt.Sprintf("[%s]\\n%s", r.escapeString(f.Condition), label)
144+
}
145+
146+
// Add annotation indicator if present and enabled
147+
if r.ShowAnnotations && f.HasAnnotations() {
148+
label = fmt.Sprintf("%s\\n(%d annotations)", label, len(f.Annotations))
149+
}
150+
132151
fmt.Fprintf(sb, " %s -> %s [label=\"%d. %s\"%s];\n",
133152
f.From, f.To, i+1, label, style)
153+
154+
// Render alternative edges if present
155+
for _, alt := range f.Alternatives {
156+
for j, altFlow := range alt.Flows {
157+
altStyle := r.modeToStyle(altFlow.EffectiveMode())
158+
altLabel := r.escapeString(altFlow.DisplayLabel())
159+
if r.ShowConditions {
160+
altLabel = fmt.Sprintf("[ALT: %s]\\n%s", r.escapeString(alt.Condition), altLabel)
161+
}
162+
fmt.Fprintf(sb, " %s -> %s [label=\"%d.%d. %s\"%s, color=gray];\n",
163+
altFlow.From, altFlow.To, i+1, j+1, altLabel, altStyle)
164+
}
165+
}
134166
}
135167
}
136168

0 commit comments

Comments
 (0)