2929 autonumberRegex = regexp .MustCompile (`^\s*autonumber\s*$` )
3030
3131 // fragmentStartRegex matches the opening line of a control-flow fragment,
32- // e.g. "loop every minute" or "opt is premium". Group 1 is the keyword,
33- // group 2 is the (optional) label describing the condition.
34- fragmentStartRegex = regexp .MustCompile (`^\s*(loop|opt)\b\s*(.*)$` )
32+ // e.g. "loop every minute", "opt is premium", "alt is valid". Group 1 is the
33+ // keyword, group 2 is the (optional) label describing the condition.
34+ fragmentStartRegex = regexp .MustCompile (`^\s*(loop|opt|alt)\b\s*(.*)$` )
35+
36+ // fragmentElseRegex matches an "else" divider inside an alt block. Group 1 is
37+ // the (optional) condition label for the following section.
38+ fragmentElseRegex = regexp .MustCompile (`^\s*else\b\s*(.*)$` )
3539
3640 // fragmentEndRegex matches the "end" line that closes a fragment.
3741 fragmentEndRegex = regexp .MustCompile (`^\s*end\s*$` )
@@ -63,6 +67,7 @@ type FragmentType int
6367const (
6468 FragmentLoop FragmentType = iota // loop ... end
6569 FragmentOpt // opt ... end
70+ FragmentAlt // alt ... else ... end
6671)
6772
6873func (f FragmentType ) String () string {
@@ -71,6 +76,8 @@ func (f FragmentType) String() string {
7176 return "loop"
7277 case FragmentOpt :
7378 return "opt"
79+ case FragmentAlt :
80+ return "alt"
7481 default :
7582 return fmt .Sprintf ("FragmentType(%d)" , int (f ))
7683 }
@@ -87,12 +94,30 @@ type Fragment struct {
8794type EventKind int
8895
8996const (
90- EventMessage EventKind = iota // a message arrow
91- EventFragmentStart // the opening line of a loop/opt block
92- EventFragmentEnd // the matching "end" line
93- EventNote // a note annotation
97+ EventMessage EventKind = iota // a message arrow
98+ EventFragmentStart // the opening line of a loop/opt/alt block
99+ EventFragmentDivider // an "else" section divider within an alt
100+ EventFragmentEnd // the matching "end" line
101+ EventNote // a note annotation
94102)
95103
104+ func (k EventKind ) String () string {
105+ switch k {
106+ case EventMessage :
107+ return "message"
108+ case EventFragmentStart :
109+ return "fragment-start"
110+ case EventFragmentDivider :
111+ return "fragment-divider"
112+ case EventFragmentEnd :
113+ return "fragment-end"
114+ case EventNote :
115+ return "note"
116+ default :
117+ return fmt .Sprintf ("EventKind(%d)" , int (k ))
118+ }
119+ }
120+
96121// Event is one item in the diagram body. Exactly one payload field is set:
97122// Message when Kind is EventMessage, Fragment when Kind is EventFragmentStart,
98123// Note when Kind is EventNote. An EventFragmentEnd carries no payload; it just
@@ -205,10 +230,10 @@ func Parse(input string) (*SequenceDiagram, error) {
205230 Autonumber : false ,
206231 }
207232 participantMap := make (map [string ]* Participant )
208- // openFragments counts how many loop/opt blocks are currently open so we can
209- // reject an "end" with no matching opener and, at the very end, an opener
210- // with no matching "end".
211- openFragments := 0
233+ // openFragments is a stack of the fragment types currently open, so we can
234+ // reject an "end"/"else" with no matching opener, validate that "else" only
235+ // appears inside an "alt", and detect an opener with no matching "end".
236+ var openFragments [] FragmentType
212237
213238 for i , line := range lines {
214239 trimmed := strings .TrimSpace (line )
@@ -274,35 +299,44 @@ func Parse(input string) (*SequenceDiagram, error) {
274299 continue
275300 }
276301
277- // A fragment opener ("loop"/"opt") starts a framed block.
302+ // A fragment opener ("loop"/"opt"/"alt" ) starts a framed block.
278303 if match := fragmentStartRegex .FindStringSubmatch (trimmed ); match != nil {
279- fType := FragmentLoop
280- if match [1 ] == "opt" {
281- fType = FragmentOpt
282- }
304+ fType := map [string ]FragmentType {"loop" : FragmentLoop , "opt" : FragmentOpt , "alt" : FragmentAlt }[match [1 ]]
283305 sd .Events = append (sd .Events , Event {
284306 Kind : EventFragmentStart ,
285307 Fragment : & Fragment {Type : fType , Label : strings .TrimSpace (match [2 ])},
286308 })
287- openFragments ++
309+ openFragments = append (openFragments , fType )
310+ continue
311+ }
312+
313+ // "else" divides an alt block into sections.
314+ if match := fragmentElseRegex .FindStringSubmatch (trimmed ); match != nil {
315+ if len (openFragments ) == 0 || openFragments [len (openFragments )- 1 ] != FragmentAlt {
316+ return nil , fmt .Errorf ("line %d: %q outside an alt block" , i + 2 , trimmed )
317+ }
318+ sd .Events = append (sd .Events , Event {
319+ Kind : EventFragmentDivider ,
320+ Fragment : & Fragment {Type : FragmentAlt , Label : strings .TrimSpace (match [1 ])},
321+ })
288322 continue
289323 }
290324
291325 // "end" closes the most recently opened fragment.
292326 if fragmentEndRegex .MatchString (trimmed ) {
293- if openFragments == 0 {
294- return nil , fmt .Errorf ("line %d: %q without matching loop/opt" , i + 2 , trimmed )
327+ if len ( openFragments ) == 0 {
328+ return nil , fmt .Errorf ("line %d: %q without matching loop/opt/alt " , i + 2 , trimmed )
295329 }
296330 sd .Events = append (sd .Events , Event {Kind : EventFragmentEnd })
297- openFragments --
331+ openFragments = openFragments [: len ( openFragments ) - 1 ]
298332 continue
299333 }
300334
301335 return nil , fmt .Errorf ("line %d: invalid syntax: %q" , i + 2 , trimmed )
302336 }
303337
304- if openFragments > 0 {
305- return nil , fmt .Errorf ("unclosed loop/opt fragment: missing %d \" end\" " , openFragments )
338+ if len ( openFragments ) > 0 {
339+ return nil , fmt .Errorf ("unclosed fragment: missing %d \" end\" " , len ( openFragments ) )
306340 }
307341
308342 if len (sd .Participants ) == 0 {
0 commit comments