7
7
"io"
8
8
"os"
9
9
"path/filepath"
10
+ "slices"
10
11
"sort"
11
12
"strings"
12
13
@@ -20,6 +21,7 @@ import (
20
21
type MermaidWriter struct {
21
22
MinEdgeName string
22
23
SpecifiedPackageName string
24
+ DrawV0Semantics bool
23
25
}
24
26
25
27
type MermaidOption func (* MermaidWriter )
@@ -32,6 +34,7 @@ func NewMermaidWriter(opts ...MermaidOption) *MermaidWriter {
32
34
m := & MermaidWriter {
33
35
MinEdgeName : minEdgeName ,
34
36
SpecifiedPackageName : specifiedPackageName ,
37
+ DrawV0Semantics : true ,
35
38
}
36
39
37
40
for _ , opt := range opts {
@@ -52,6 +55,12 @@ func WithSpecifiedPackageName(specifiedPackageName string) MermaidOption {
52
55
}
53
56
}
54
57
58
+ func WithV0Semantics (drawV0Semantics bool ) MermaidOption {
59
+ return func (o * MermaidWriter ) {
60
+ o .DrawV0Semantics = drawV0Semantics
61
+ }
62
+ }
63
+
55
64
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
56
65
// mermaid renderers like github, mermaid.live, etc.
57
66
// output is sorted lexicographically by package name, and then by channel name
@@ -124,7 +133,10 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
124
133
}
125
134
126
135
var deprecatedPackage string
127
- deprecatedChannels := []string {}
136
+ deprecatedChannelIDs := []string {}
137
+ decoratedBundleIDs := map [string ][]string {"deprecated" : {}, "skipped" : {}, "deprecatedskipped" : {}}
138
+ linkID := 0
139
+ skippedLinkIDs := []string {}
128
140
129
141
for _ , c := range cfg .Channels {
130
142
filteredChannel := writer .filterChannel (& c , versionMap , minVersion , minEdgePackage )
@@ -137,58 +149,102 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
137
149
}
138
150
139
151
channelID := fmt .Sprintf ("%s-%s" , filteredChannel .Package , filteredChannel .Name )
140
- pkgBuilder . WriteString ( fmt .Sprintf ( " %%%% channel %q\n " , filteredChannel .Name ) )
141
- pkgBuilder . WriteString ( fmt .Sprintf ( " subgraph %s[%q]\n " , channelID , filteredChannel .Name ) )
152
+ fmt .Fprintf ( pkgBuilder , " %%%% channel %q\n " , filteredChannel .Name )
153
+ fmt .Fprintf ( pkgBuilder , " subgraph %s[%q]\n " , channelID , filteredChannel .Name )
142
154
143
155
if depByPackage .Has (filteredChannel .Package ) {
144
156
deprecatedPackage = filteredChannel .Package
145
157
}
146
158
147
159
if depByChannel .Has (filteredChannel .Name ) {
148
- deprecatedChannels = append (deprecatedChannels , channelID )
160
+ deprecatedChannelIDs = append (deprecatedChannelIDs , channelID )
149
161
}
150
162
151
- for _ , ce := range filteredChannel .Entries {
152
- if versionMap [ce .Name ].GE (minVersion ) {
153
- bundleDeprecation := ""
154
- if depByBundle .Has (ce .Name ) {
155
- bundleDeprecation = ":::deprecated"
163
+ // sort edges by decreasing version
164
+ sortedEntries := make ([]* ChannelEntry , 0 , len (filteredChannel .Entries ))
165
+ for i := range filteredChannel .Entries {
166
+ sortedEntries = append (sortedEntries , & filteredChannel .Entries [i ])
167
+ }
168
+ sort .Slice (sortedEntries , func (i , j int ) bool {
169
+ // Sort by decreasing version: greater version comes first
170
+ return versionMap [sortedEntries [i ].Name ].GT (versionMap [sortedEntries [j ].Name ])
171
+ })
172
+
173
+ skippedEntities := sets.Set [string ]{}
174
+
175
+ const (
176
+ captureNewEntry = true
177
+ processExisting = false
178
+ )
179
+ handleSemantics := func (edge string , linkID int , captureNew bool ) {
180
+ if writer .DrawV0Semantics {
181
+ if captureNew {
182
+ if skippedEntities .Has (edge ) {
183
+ skippedLinkIDs = append (skippedLinkIDs , fmt .Sprintf ("%d" , linkID ))
184
+ } else {
185
+ skippedEntities .Insert (edge )
186
+ }
187
+ } else {
188
+ if skippedEntities .Has (edge ) {
189
+ skippedLinkIDs = append (skippedLinkIDs , fmt .Sprintf ("%d" , linkID ))
190
+ }
156
191
}
192
+ }
193
+ }
157
194
158
- entryID := fmt .Sprintf ("%s-%s" , channelID , ce .Name )
159
- pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]%s\n " , entryID , ce .Name , bundleDeprecation ))
195
+ for _ , ce := range sortedEntries {
196
+ entryID := fmt .Sprintf ("%s-%s" , channelID , ce .Name )
197
+ fmt .Fprintf (pkgBuilder , " %s[%q]\n " , entryID , ce .Name )
198
+
199
+ // mermaid allows specification of only a single decoration class, so any combinations must be independently represented
200
+ switch {
201
+ case depByBundle .Has (ce .Name ) && skippedEntities .Has (ce .Name ):
202
+ decoratedBundleIDs ["deprecatedskipped" ] = append (decoratedBundleIDs ["deprecatedskipped" ], entryID )
203
+ case depByBundle .Has (ce .Name ):
204
+ decoratedBundleIDs ["deprecated" ] = append (decoratedBundleIDs ["deprecated" ], entryID )
205
+ case skippedEntities .Has (ce .Name ):
206
+ decoratedBundleIDs ["skipped" ] = append (decoratedBundleIDs ["skipped" ], entryID )
207
+ }
160
208
161
- if len (ce .Replaces ) > 0 {
162
- replacesID := fmt .Sprintf ("%s-%s" , channelID , ce .Replaces )
163
- pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]-- %s --> %s[%q]\n " , replacesID , ce .Replaces , "replace" , entryID , ce .Name ))
164
- }
165
- if len (ce .Skips ) > 0 {
166
- for _ , s := range ce .Skips {
167
- skipsID := fmt .Sprintf ("%s-%s" , channelID , s )
168
- pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]-- %s --> %s[%q]\n " , skipsID , s , "skip" , entryID , ce .Name ))
169
- }
209
+ if len (ce .Skips ) > 0 {
210
+ for _ , s := range ce .Skips {
211
+ skipsID := fmt .Sprintf ("%s-%s" , channelID , s )
212
+ fmt .Fprintf (pkgBuilder , " %s[%q]-- %s --> %s[%q]\n " , skipsID , s , "skip" , entryID , ce .Name )
213
+ handleSemantics (s , linkID , captureNewEntry )
214
+ linkID ++
170
215
}
171
- if len (ce .SkipRange ) > 0 {
172
- skipRange , err := semver .ParseRange (ce .SkipRange )
173
- if err == nil {
174
- for _ , edgeName := range filteredChannel .Entries {
175
- if skipRange (versionMap [edgeName .Name ]) {
176
- skipRangeID := fmt .Sprintf ("%s-%s" , channelID , edgeName .Name )
177
- pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]-- \" %s(%s)\" --> %s[%q]\n " , skipRangeID , edgeName .Name , "skipRange" , ce .SkipRange , entryID , ce .Name ))
178
- }
216
+ }
217
+ if len (ce .SkipRange ) > 0 {
218
+ skipRange , err := semver .ParseRange (ce .SkipRange )
219
+ if err == nil {
220
+ for _ , edgeName := range filteredChannel .Entries {
221
+ if skipRange (versionMap [edgeName .Name ]) {
222
+ skipRangeID := fmt .Sprintf ("%s-%s" , channelID , edgeName .Name )
223
+ fmt .Fprintf (pkgBuilder , " %s[%q]-- \" %s(%s)\" --> %s[%q]\n " , skipRangeID , edgeName .Name , "skipRange" , ce .SkipRange , entryID , ce .Name )
224
+ handleSemantics (ce .Name , linkID , processExisting )
225
+ linkID ++
179
226
}
180
- } else {
181
- fmt .Fprintf (os .Stderr , "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n " , c .Package , ce .Name , err )
182
227
}
228
+ } else {
229
+ fmt .Fprintf (os .Stderr , "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n " , c .Package , ce .Name , err )
183
230
}
184
231
}
232
+ // have to process replaces last, because applicablity can be impacted by skips
233
+ if len (ce .Replaces ) > 0 {
234
+ replacesID := fmt .Sprintf ("%s-%s" , channelID , ce .Replaces )
235
+ fmt .Fprintf (pkgBuilder , " %s[%q]-- %s --> %s[%q]\n " , replacesID , ce .Replaces , "replace" , entryID , ce .Name )
236
+ handleSemantics (ce .Name , linkID , processExisting )
237
+ linkID ++
238
+ }
185
239
}
186
- pkgBuilder . WriteString ( " end\n " )
240
+ fmt . Fprintf ( pkgBuilder , " end\n " )
187
241
}
188
242
}
189
243
190
244
_ , _ = out .Write ([]byte ("graph LR\n " ))
191
245
_ , _ = out .Write ([]byte (" classDef deprecated fill:#E8960F\n " ))
246
+ _ , _ = out .Write ([]byte (" classDef skipped stroke:#FF0000,stroke-width:4px\n " ))
247
+ _ , _ = out .Write ([]byte (" classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px\n " ))
192
248
pkgNames := []string {}
193
249
for pname := range pkgs {
194
250
pkgNames = append (pkgNames , pname )
@@ -197,22 +253,35 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
197
253
return pkgNames [i ] < pkgNames [j ]
198
254
})
199
255
for _ , pkgName := range pkgNames {
200
- _ , _ = out . Write ([] byte ( fmt .Sprintf ( " %%%% package %q\n " , pkgName )) )
201
- _ , _ = out . Write ([] byte ( fmt .Sprintf ( " subgraph %q\n " , pkgName )) )
256
+ _ , _ = fmt .Fprintf ( out , " %%%% package %q\n " , pkgName )
257
+ _ , _ = fmt .Fprintf ( out , " subgraph %q\n " , pkgName )
202
258
_ , _ = out .Write ([]byte (pkgs [pkgName ].String ()))
203
259
_ , _ = out .Write ([]byte (" end\n " ))
204
260
}
205
261
206
262
if deprecatedPackage != "" {
207
- _ , _ = out .Write ([]byte (fmt .Sprintf ("style %s fill:#989695\n " , deprecatedPackage )))
263
+ _ , _ = fmt .Fprintf (out , "style %s fill:#989695\n " , deprecatedPackage )
264
+ }
265
+
266
+ if len (deprecatedChannelIDs ) > 0 {
267
+ for _ , deprecatedChannel := range deprecatedChannelIDs {
268
+ _ , _ = fmt .Fprintf (out , "style %s fill:#DCD0FF\n " , deprecatedChannel )
269
+ }
208
270
}
209
271
210
- if len (deprecatedChannels ) > 0 {
211
- for _ , deprecatedChannel := range deprecatedChannels {
212
- _ , _ = out .Write ([]byte (fmt .Sprintf ("style %s fill:#DCD0FF\n " , deprecatedChannel )))
272
+ // express the decoration classes
273
+ for key := range decoratedBundleIDs {
274
+ if len (decoratedBundleIDs [key ]) > 0 {
275
+ b := slices .Clone (decoratedBundleIDs [key ])
276
+ slices .Sort (b )
277
+ _ , _ = fmt .Fprintf (out , "class %s %s\n " , strings .Join (b , "," ), key )
213
278
}
214
279
}
215
280
281
+ if len (skippedLinkIDs ) > 0 {
282
+ _ , _ = fmt .Fprintf (out , "linkStyle %s %s\n " , strings .Join (skippedLinkIDs , "," ), "stroke:#FF0000,stroke-width:3px,stroke-dasharray:5;" )
283
+ }
284
+
216
285
return nil
217
286
}
218
287
0 commit comments