@@ -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.
174290func (r * D2Renderer ) renderFlow (p * pidl.Protocol ) string {
175291 var sb strings.Builder
0 commit comments