@@ -7,13 +7,94 @@ import (
77 "bytes"
88 "fmt"
99 "io"
10+ "os"
11+ "regexp"
1012 "sort"
13+ "strconv"
1114 "strings"
1215 "text/template"
16+ "unicode/utf8"
1317
1418 "github.com/cpuguy83/go-md2man/v2/md2man"
1519)
1620
21+ // ToTabularMarkdown creates a tabular markdown documentation for the `*App`.
22+ // The function errors if either parsing or writing of the string fails.
23+ func (a * App ) ToTabularMarkdown (appPath string ) (string , error ) {
24+ if appPath == "" {
25+ appPath = "app"
26+ }
27+
28+ const name = "cli"
29+
30+ t , err := template .New (name ).Funcs (template.FuncMap {
31+ "join" : strings .Join ,
32+ }).Parse (MarkdownTabularDocTemplate )
33+ if err != nil {
34+ return "" , err
35+ }
36+
37+ var (
38+ w bytes.Buffer
39+ tt tabularTemplate
40+ )
41+
42+ if err = t .ExecuteTemplate (& w , name , cliTabularAppTemplate {
43+ AppPath : appPath ,
44+ Name : a .Name ,
45+ Description : tt .PrepareMultilineString (a .Description ),
46+ Usage : tt .PrepareMultilineString (a .Usage ),
47+ UsageText : strings .FieldsFunc (a .UsageText , func (r rune ) bool { return r == '\n' }),
48+ ArgsUsage : tt .PrepareMultilineString (a .ArgsUsage ),
49+ GlobalFlags : tt .PrepareFlags (a .VisibleFlags ()),
50+ Commands : tt .PrepareCommands (a .VisibleCommands (), appPath , "" , 0 ),
51+ }); err != nil {
52+ return "" , err
53+ }
54+
55+ return tt .Prettify (w .String ()), nil
56+ }
57+
58+ // ToTabularToFileBetweenTags creates a tabular markdown documentation for the `*App` and updates the file between
59+ // the tags in the file. The function errors if either parsing or writing of the string fails.
60+ func (a * App ) ToTabularToFileBetweenTags (appPath , filePath string , startEndTags ... string ) error {
61+ var start , end = "<!--GENERATED:CLI_DOCS-->" , "<!--/GENERATED:CLI_DOCS-->" // default tags
62+
63+ if len (startEndTags ) == 2 {
64+ start , end = startEndTags [0 ], startEndTags [1 ]
65+ }
66+
67+ // read original file content
68+ content , err := os .ReadFile (filePath )
69+ if err != nil {
70+ return err
71+ }
72+
73+ // generate markdown
74+ md , err := a .ToTabularMarkdown (appPath )
75+ if err != nil {
76+ return err
77+ }
78+
79+ // prepare regexp to replace content between start and end tags
80+ re , err := regexp .Compile ("(?s)" + regexp .QuoteMeta (start ) + "(.*?)" + regexp .QuoteMeta (end ))
81+ if err != nil {
82+ return err
83+ }
84+
85+ const comment = "<!-- Documentation inside this block generated by github.com/urfave/cli; DO NOT EDIT -->"
86+
87+ // replace content between start and end tags
88+ updated := re .ReplaceAll (content , []byte (strings .Join ([]string {start , comment , md , end }, "\n " )))
89+
90+ // write updated content to file
91+ if err = os .WriteFile (filePath , updated , 0664 ); err != nil {
92+ return err
93+ }
94+
95+ return nil
96+ }
97+
1798// ToMarkdown creates a markdown string for the `*App`
1899// The function errors if either parsing or writing of the string fails.
19100func (a * App ) ToMarkdown () (string , error ) {
@@ -196,3 +277,237 @@ func prepareUsage(command *Command, usageText string) string {
196277
197278 return usage
198279}
280+
281+ type (
282+ cliTabularAppTemplate struct {
283+ AppPath string
284+ Name string
285+ Usage string
286+ ArgsUsage string
287+ UsageText []string
288+ Description string
289+ GlobalFlags []cliTabularFlagTemplate
290+ Commands []cliTabularCommandTemplate
291+ }
292+
293+ cliTabularCommandTemplate struct {
294+ AppPath string
295+ Name string
296+ Aliases []string
297+ Usage string
298+ ArgsUsage string
299+ UsageText []string
300+ Description string
301+ Category string
302+ Flags []cliTabularFlagTemplate
303+ SubCommands []cliTabularCommandTemplate
304+ Level uint
305+ }
306+
307+ cliTabularFlagTemplate struct {
308+ Name string
309+ Aliases []string
310+ Usage string
311+ TakesValue bool
312+ Default string
313+ EnvVars []string
314+ }
315+ )
316+
317+ // tabularTemplate is a struct for the tabular template preparation.
318+ type tabularTemplate struct {}
319+
320+ // PrepareCommands converts CLI commands into a structs for the rendering.
321+ func (tt tabularTemplate ) PrepareCommands (commands []* Command , appPath , parentCommandName string , level uint ) []cliTabularCommandTemplate {
322+ var result = make ([]cliTabularCommandTemplate , 0 , len (commands ))
323+
324+ for _ , cmd := range commands {
325+ var command = cliTabularCommandTemplate {
326+ AppPath : appPath ,
327+ Name : strings .TrimSpace (strings .Join ([]string {parentCommandName , cmd .Name }, " " )),
328+ Aliases : cmd .Aliases ,
329+ Usage : tt .PrepareMultilineString (cmd .Usage ),
330+ UsageText : strings .FieldsFunc (cmd .UsageText , func (r rune ) bool { return r == '\n' }),
331+ ArgsUsage : tt .PrepareMultilineString (cmd .ArgsUsage ),
332+ Description : tt .PrepareMultilineString (cmd .Description ),
333+ Category : cmd .Category ,
334+ Flags : tt .PrepareFlags (cmd .VisibleFlags ()),
335+ SubCommands : tt .PrepareCommands ( // note: recursive call
336+ cmd .Commands ,
337+ appPath ,
338+ strings .Join ([]string {parentCommandName , cmd .Name }, " " ),
339+ level + 1 ,
340+ ),
341+ Level : level ,
342+ }
343+
344+ result = append (result , command )
345+ }
346+
347+ return result
348+ }
349+
350+ // PrepareFlags converts CLI flags into a structs for the rendering.
351+ func (tt tabularTemplate ) PrepareFlags (flags []Flag ) []cliTabularFlagTemplate {
352+ var result = make ([]cliTabularFlagTemplate , 0 , len (flags ))
353+
354+ for _ , appFlag := range flags {
355+ flag , ok := appFlag .(DocGenerationFlag )
356+ if ! ok {
357+ continue
358+ }
359+
360+ var f = cliTabularFlagTemplate {
361+ Usage : tt .PrepareMultilineString (flag .GetUsage ()),
362+ EnvVars : flag .GetEnvVars (),
363+ TakesValue : flag .TakesValue (),
364+ Default : flag .GetValue (),
365+ }
366+
367+ if boolFlag , isBool := appFlag .(* BoolFlag ); isBool {
368+ f .Default = strconv .FormatBool (boolFlag .Value )
369+ }
370+
371+ for i , name := range flag .Names () {
372+ name = strings .TrimSpace (name )
373+
374+ if i == 0 {
375+ f .Name = "--" + name
376+
377+ continue
378+ }
379+
380+ if len (name ) > 1 {
381+ name = "--" + name
382+ } else {
383+ name = "-" + name
384+ }
385+
386+ f .Aliases = append (f .Aliases , name )
387+ }
388+
389+ result = append (result , f )
390+ }
391+
392+ return result
393+ }
394+
395+ // PrepareMultilineString prepares a string (removes line breaks).
396+ func (tabularTemplate ) PrepareMultilineString (s string ) string {
397+ return strings .TrimRight (
398+ strings .TrimSpace (
399+ strings .ReplaceAll (s , "\n " , " " ),
400+ ),
401+ ".\r \n \t " ,
402+ )
403+ }
404+
405+ func (tabularTemplate ) Prettify (s string ) string {
406+ var max = func (x , y int ) int {
407+ if x > y {
408+ return x
409+ }
410+ return y
411+ }
412+
413+ var b strings.Builder
414+
415+ // search for tables
416+ for _ , rawTable := range regexp .MustCompile (`(?m)^(\|[^\n]+\|\r?\n)((?:\|:?-+:?)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$` ).FindAllString (s , - 1 ) {
417+ var lines = strings .FieldsFunc (rawTable , func (r rune ) bool { return r == '\n' })
418+
419+ if len (lines ) < 3 { // header, separator, body
420+ continue
421+ }
422+
423+ // parse table into the matrix
424+ var matrix = make ([][]string , 0 , len (lines ))
425+ for _ , line := range lines {
426+ items := strings .FieldsFunc (strings .Trim (line , "| " ), func (r rune ) bool { return r == '|' })
427+
428+ for i := range items {
429+ items [i ] = strings .TrimSpace (items [i ]) // trim spaces in cells
430+ }
431+
432+ matrix = append (matrix , items )
433+ }
434+
435+ // determine centered columns
436+ var centered = make ([]bool , 0 , len (matrix [1 ]))
437+ for _ , cell := range matrix [1 ] {
438+ centered = append (centered , strings .HasPrefix (cell , ":" ) && strings .HasSuffix (cell , ":" ))
439+ }
440+
441+ // calculate max lengths
442+ var lengths = make ([]int , len (matrix [0 ]))
443+ for n , row := range matrix {
444+ for i , cell := range row {
445+ if n == 1 {
446+ continue // skip separator
447+ }
448+
449+ if l := utf8 .RuneCountInString (cell ); l > lengths [i ] {
450+ lengths [i ] = l
451+ }
452+ }
453+ }
454+
455+ // format cells
456+ for i , row := range matrix {
457+ for j , cell := range row {
458+ if i == 1 { // is separator
459+ if centered [j ] {
460+ b .Reset ()
461+ b .WriteRune (':' )
462+ b .WriteString (strings .Repeat ("-" , max (0 , lengths [j ])))
463+ b .WriteRune (':' )
464+
465+ row [j ] = b .String ()
466+ } else {
467+ row [j ] = strings .Repeat ("-" , max (0 , lengths [j ]+ 2 ))
468+ }
469+
470+ continue
471+ }
472+
473+ var (
474+ cellWidth = utf8 .RuneCountInString (cell )
475+ padLeft , padRight = 1 , max (1 , lengths [j ]- cellWidth + 1 ) // align to the left
476+ )
477+
478+ if centered [j ] { // is centered
479+ padLeft = max (1 , (lengths [j ]- cellWidth )/ 2 )
480+ padRight = max (1 , lengths [j ]- cellWidth - (padLeft - 1 ))
481+ }
482+
483+ b .Reset ()
484+ b .WriteString (strings .Repeat (" " , padLeft ))
485+
486+ if padLeft + cellWidth + padRight <= lengths [j ]+ 1 {
487+ b .WriteRune (' ' ) // add an extra space if the cell is not full
488+ }
489+
490+ b .WriteString (cell )
491+ b .WriteString (strings .Repeat (" " , padRight ))
492+
493+ row [j ] = b .String ()
494+ }
495+ }
496+
497+ b .Reset ()
498+
499+ for _ , row := range matrix { // build new table
500+ b .WriteRune ('|' )
501+ b .WriteString (strings .Join (row , "|" ))
502+ b .WriteRune ('|' )
503+ b .WriteRune ('\n' )
504+ }
505+
506+ s = strings .Replace (s , rawTable , b .String (), 1 )
507+ }
508+
509+ s = regexp .MustCompile (`\n{2,}` ).ReplaceAllString (s , "\n \n " ) // normalize newlines
510+ s = strings .Trim (s , " \n " ) // trim spaces and newlines
511+
512+ return s + "\n " // add an extra newline
513+ }
0 commit comments