@@ -7,13 +7,70 @@ import (
7
7
"bytes"
8
8
"fmt"
9
9
"io"
10
+ "regexp"
10
11
"sort"
12
+ "strconv"
11
13
"strings"
12
14
"text/template"
15
+ "unicode/utf8"
13
16
14
17
"github.com/cpuguy83/go-md2man/v2/md2man"
15
18
)
16
19
20
+ type (
21
+ tabularOptions struct {
22
+ appPath string
23
+ }
24
+
25
+ TabularOption func (* tabularOptions )
26
+ )
27
+
28
+ // WithTabularAppPath allows to override the default app path.
29
+ func WithTabularAppPath (path string ) TabularOption {
30
+ return func (o * tabularOptions ) { o .appPath = path }
31
+ }
32
+
33
+ // ToTabularMarkdown creates a tabular markdown documentation for the `*App`.
34
+ // The function errors if either parsing or writing of the string fails.
35
+ func (a * App ) ToTabularMarkdown (opts ... TabularOption ) (string , error ) {
36
+ var o = tabularOptions {
37
+ appPath : "app" ,
38
+ }
39
+
40
+ for _ , opt := range opts {
41
+ opt (& o )
42
+ }
43
+
44
+ const name = "cli"
45
+
46
+ t , err := template .New (name ).Funcs (template.FuncMap {
47
+ "join" : strings .Join ,
48
+ }).Parse (MarkdownTabularDocTemplate )
49
+ if err != nil {
50
+ return "" , err
51
+ }
52
+
53
+ var (
54
+ w bytes.Buffer
55
+ tt tabularTemplate
56
+ )
57
+
58
+ if err = t .ExecuteTemplate (& w , name , cliTabularAppTemplate {
59
+ AppPath : o .appPath ,
60
+ Name : a .Name ,
61
+ Description : tt .PrepareMultilineString (a .Description ),
62
+ Usage : tt .PrepareMultilineString (a .Usage ),
63
+ UsageText : tt .PrepareMultilineString (a .UsageText ),
64
+ ArgsUsage : tt .PrepareMultilineString (a .ArgsUsage ),
65
+ GlobalFlags : tt .PrepareFlags (a .VisibleFlags ()),
66
+ Commands : tt .PrepareCommands (a .VisibleCommands (), o .appPath , "" , 0 ),
67
+ }); err != nil {
68
+ return "" , err
69
+ }
70
+
71
+ return tt .Prettify (w .String ()), nil
72
+ }
73
+
17
74
// ToMarkdown creates a markdown string for the `*App`
18
75
// The function errors if either parsing or writing of the string fails.
19
76
func (a * App ) ToMarkdown () (string , error ) {
@@ -196,3 +253,204 @@ func prepareUsage(command *Command, usageText string) string {
196
253
197
254
return usage
198
255
}
256
+
257
+ type (
258
+ cliTabularAppTemplate struct {
259
+ AppPath string
260
+ Name string
261
+ Usage , UsageText , ArgsUsage string
262
+ Description string
263
+ GlobalFlags []cliTabularFlagTemplate
264
+ Commands []cliTabularCommandTemplate
265
+ }
266
+
267
+ cliTabularCommandTemplate struct {
268
+ AppPath string
269
+ Name string
270
+ Aliases []string
271
+ Usage , UsageText , ArgsUsage string
272
+ Description string
273
+ Category string
274
+ Flags []cliTabularFlagTemplate
275
+ SubCommands []cliTabularCommandTemplate
276
+ Level uint
277
+ }
278
+
279
+ cliTabularFlagTemplate struct {
280
+ Name string
281
+ Aliases []string
282
+ Usage string
283
+ TakesValue bool
284
+ Default string
285
+ EnvVars []string
286
+ }
287
+ )
288
+
289
+ // tabularTemplate is a struct for the tabular template preparation.
290
+ type tabularTemplate struct {}
291
+
292
+ // PrepareCommands converts CLI commands into a structs for the rendering.
293
+ func (tt tabularTemplate ) PrepareCommands (commands []* Command , appPath , parentCommandName string , level uint ) []cliTabularCommandTemplate {
294
+ var result = make ([]cliTabularCommandTemplate , 0 , len (commands ))
295
+
296
+ for _ , cmd := range commands {
297
+ var command = cliTabularCommandTemplate {
298
+ AppPath : appPath ,
299
+ Name : strings .TrimSpace (strings .Join ([]string {parentCommandName , cmd .Name }, " " )),
300
+ Aliases : cmd .Aliases ,
301
+ Usage : tt .PrepareMultilineString (cmd .Usage ),
302
+ UsageText : tt .PrepareMultilineString (cmd .UsageText ),
303
+ ArgsUsage : tt .PrepareMultilineString (cmd .ArgsUsage ),
304
+ Description : tt .PrepareMultilineString (cmd .Description ),
305
+ Category : cmd .Category ,
306
+ Flags : tt .PrepareFlags (cmd .VisibleFlags ()),
307
+ SubCommands : tt .PrepareCommands ( // note: recursive call
308
+ cmd .Commands ,
309
+ appPath ,
310
+ strings .Join ([]string {parentCommandName , cmd .Name }, " " ),
311
+ level + 1 ,
312
+ ),
313
+ Level : level ,
314
+ }
315
+
316
+ result = append (result , command )
317
+ }
318
+
319
+ return result
320
+ }
321
+
322
+ // PrepareFlags converts CLI flags into a structs for the rendering.
323
+ func (tt tabularTemplate ) PrepareFlags (flags []Flag ) []cliTabularFlagTemplate {
324
+ var result = make ([]cliTabularFlagTemplate , 0 , len (flags ))
325
+
326
+ for _ , appFlag := range flags {
327
+ flag , ok := appFlag .(DocGenerationFlag )
328
+ if ! ok {
329
+ continue
330
+ }
331
+
332
+ var f = cliTabularFlagTemplate {
333
+ Usage : tt .PrepareMultilineString (flag .GetUsage ()),
334
+ EnvVars : flag .GetEnvVars (),
335
+ TakesValue : flag .TakesValue (),
336
+ Default : flag .GetValue (),
337
+ }
338
+
339
+ if boolFlag , isBool := appFlag .(* BoolFlag ); isBool {
340
+ f .Default = strconv .FormatBool (boolFlag .Value )
341
+ }
342
+
343
+ for i , name := range flag .Names () {
344
+ name = strings .TrimSpace (name )
345
+
346
+ if i == 0 {
347
+ f .Name = "--" + name
348
+
349
+ continue
350
+ }
351
+
352
+ if len (name ) > 1 {
353
+ name = "--" + name
354
+ } else {
355
+ name = "-" + name
356
+ }
357
+
358
+ f .Aliases = append (f .Aliases , name )
359
+ }
360
+
361
+ result = append (result , f )
362
+ }
363
+
364
+ return result
365
+ }
366
+
367
+ // PrepareMultilineString prepares a string (removes line breaks).
368
+ func (tabularTemplate ) PrepareMultilineString (s string ) string {
369
+ return strings .TrimRight (
370
+ strings .TrimSpace (
371
+ strings .ReplaceAll (s , "\n " , " " ),
372
+ ),
373
+ ".\r \n \t " ,
374
+ )
375
+ }
376
+
377
+ func (tabularTemplate ) Prettify (s string ) string {
378
+ s = regexp .MustCompile (`\n{2,}` ).ReplaceAllString (s , "\n \n " ) // normalize newlines
379
+ s = strings .Trim (s , " \n " ) // trim spaces and newlines
380
+
381
+ // search for tables
382
+ for _ , rawTable := range regexp .MustCompile (`(?m)^(\|[^\n]+\|\r?\n)((?:\|:?-+:?)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$` ).FindAllString (s , - 1 ) {
383
+ var lines = strings .FieldsFunc (rawTable , func (r rune ) bool { return r == '\n' })
384
+
385
+ if len (lines ) < 3 { // header, separator, body
386
+ continue
387
+ }
388
+
389
+ // parse table into the matrix
390
+ var matrix = make ([][]string , 0 , len (lines ))
391
+ for _ , line := range lines {
392
+ items := strings .FieldsFunc (strings .Trim (line , "| " ), func (r rune ) bool { return r == '|' })
393
+
394
+ for i := range items {
395
+ items [i ] = strings .TrimSpace (items [i ]) // trim spaces in cells
396
+ }
397
+
398
+ matrix = append (matrix , items )
399
+ }
400
+
401
+ // determine centered columns
402
+ var centered = make ([]bool , 0 , len (matrix [1 ]))
403
+ for _ , cell := range matrix [1 ] {
404
+ centered = append (centered , strings .HasPrefix (cell , ":" ) && strings .HasSuffix (cell , ":" ))
405
+ }
406
+
407
+ // calculate max lengths
408
+ var lengths = make ([]int , len (matrix [0 ]))
409
+ const padding = 2 // 2 spaces for padding
410
+ for _ , row := range matrix {
411
+ for i , cell := range row {
412
+ if len (cell ) > lengths [i ]- padding {
413
+ lengths [i ] = utf8 .RuneCountInString (cell ) + padding
414
+ }
415
+ }
416
+ }
417
+
418
+ // format cells
419
+ for i , row := range matrix {
420
+ for j , cell := range row {
421
+ if i == 1 { // is separator
422
+ if centered [j ] {
423
+ cell = ":" + strings .Repeat ("-" , lengths [j ]- 2 ) + ":"
424
+ } else {
425
+ cell = strings .Repeat ("-" , lengths [j ]+ 1 )
426
+ }
427
+ }
428
+
429
+ var (
430
+ padLeft , padRight = 1 , 1
431
+ cellWidth = utf8 .RuneCountInString (cell )
432
+ )
433
+
434
+ if centered [j ] { // is centered
435
+ padLeft = (lengths [j ] - cellWidth ) / 2
436
+ padRight = lengths [j ] - cellWidth - padLeft
437
+ } else if i == 1 { // is header
438
+ padLeft , padRight = 0 , 0
439
+ } else { // align to the left
440
+ padRight = lengths [j ] - cellWidth
441
+ }
442
+
443
+ row [j ] = strings .Repeat (" " , padLeft ) + cell + strings .Repeat (" " , padRight )
444
+ }
445
+ }
446
+
447
+ var newTable string
448
+ for _ , row := range matrix { // build new table
449
+ newTable += "|" + strings .Join (row , "|" ) + "|\n "
450
+ }
451
+
452
+ s = strings .Replace (s , rawTable , newTable , 1 )
453
+ }
454
+
455
+ return s + "\n " // add an extra newline
456
+ }
0 commit comments