1package cli
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"sort"
8	"strings"
9	"text/template"
10
11	"github.com/cpuguy83/go-md2man/v2/md2man"
12)
13
14// ToMarkdown creates a markdown string for the `*App`
15// The function errors if either parsing or writing of the string fails.
16func (a *App) ToMarkdown() (string, error) {
17	var w bytes.Buffer
18	if err := a.writeDocTemplate(&w); err != nil {
19		return "", err
20	}
21	return w.String(), nil
22}
23
24// ToMan creates a man page string for the `*App`
25// The function errors if either parsing or writing of the string fails.
26func (a *App) ToMan() (string, error) {
27	var w bytes.Buffer
28	if err := a.writeDocTemplate(&w); err != nil {
29		return "", err
30	}
31	man := md2man.Render(w.Bytes())
32	return string(man), nil
33}
34
35type cliTemplate struct {
36	App          *App
37	Commands     []string
38	GlobalArgs   []string
39	SynopsisArgs []string
40}
41
42func (a *App) writeDocTemplate(w io.Writer) error {
43	const name = "cli"
44	t, err := template.New(name).Parse(MarkdownDocTemplate)
45	if err != nil {
46		return err
47	}
48	return t.ExecuteTemplate(w, name, &cliTemplate{
49		App:          a,
50		Commands:     prepareCommands(a.Commands, 0),
51		GlobalArgs:   prepareArgsWithValues(a.VisibleFlags()),
52		SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()),
53	})
54}
55
56func prepareCommands(commands []*Command, level int) []string {
57	var coms []string
58	for _, command := range commands {
59		if command.Hidden {
60			continue
61		}
62		usage := ""
63		if command.Usage != "" {
64			usage = command.Usage
65		}
66
67		prepared := fmt.Sprintf("%s %s\n\n%s\n",
68			strings.Repeat("#", level+2),
69			strings.Join(command.Names(), ", "),
70			usage,
71		)
72
73		flags := prepareArgsWithValues(command.Flags)
74		if len(flags) > 0 {
75			prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
76		}
77
78		coms = append(coms, prepared)
79
80		// recursevly iterate subcommands
81		if len(command.Subcommands) > 0 {
82			coms = append(
83				coms,
84				prepareCommands(command.Subcommands, level+1)...,
85			)
86		}
87	}
88
89	return coms
90}
91
92func prepareArgsWithValues(flags []Flag) []string {
93	return prepareFlags(flags, ", ", "**", "**", `""`, true)
94}
95
96func prepareArgsSynopsis(flags []Flag) []string {
97	return prepareFlags(flags, "|", "[", "]", "[value]", false)
98}
99
100func prepareFlags(
101	flags []Flag,
102	sep, opener, closer, value string,
103	addDetails bool,
104) []string {
105	args := []string{}
106	for _, f := range flags {
107		flag, ok := f.(DocGenerationFlag)
108		if !ok {
109			continue
110		}
111		modifiedArg := opener
112
113		for _, s := range flag.Names() {
114			trimmed := strings.TrimSpace(s)
115			if len(modifiedArg) > len(opener) {
116				modifiedArg += sep
117			}
118			if len(trimmed) > 1 {
119				modifiedArg += fmt.Sprintf("--%s", trimmed)
120			} else {
121				modifiedArg += fmt.Sprintf("-%s", trimmed)
122			}
123		}
124		modifiedArg += closer
125		if flag.TakesValue() {
126			modifiedArg += fmt.Sprintf("=%s", value)
127		}
128
129		if addDetails {
130			modifiedArg += flagDetails(flag)
131		}
132
133		args = append(args, modifiedArg+"\n")
134
135	}
136	sort.Strings(args)
137	return args
138}
139
140// flagDetails returns a string containing the flags metadata
141func flagDetails(flag DocGenerationFlag) string {
142	description := flag.GetUsage()
143	value := flag.GetValue()
144	if value != "" {
145		description += " (default: " + value + ")"
146	}
147	return ": " + description
148}
149