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.Flags),
52		SynopsisArgs: prepareArgsSynopsis(a.Flags),
53	})
54}
55
56func prepareCommands(commands []Command, level int) []string {
57	coms := []string{}
58	for i := range commands {
59		command := &commands[i]
60		if command.Hidden {
61			continue
62		}
63		usage := ""
64		if command.Usage != "" {
65			usage = command.Usage
66		}
67
68		prepared := fmt.Sprintf("%s %s\n\n%s\n",
69			strings.Repeat("#", level+2),
70			strings.Join(command.Names(), ", "),
71			usage,
72		)
73
74		flags := prepareArgsWithValues(command.Flags)
75		if len(flags) > 0 {
76			prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
77		}
78
79		coms = append(coms, prepared)
80
81		// recursevly iterate subcommands
82		if len(command.Subcommands) > 0 {
83			coms = append(
84				coms,
85				prepareCommands(command.Subcommands, level+1)...,
86			)
87		}
88	}
89
90	return coms
91}
92
93func prepareArgsWithValues(flags []Flag) []string {
94	return prepareFlags(flags, ", ", "**", "**", `""`, true)
95}
96
97func prepareArgsSynopsis(flags []Flag) []string {
98	return prepareFlags(flags, "|", "[", "]", "[value]", false)
99}
100
101func prepareFlags(
102	flags []Flag,
103	sep, opener, closer, value string,
104	addDetails bool,
105) []string {
106	args := []string{}
107	for _, f := range flags {
108		flag, ok := f.(DocGenerationFlag)
109		if !ok {
110			continue
111		}
112		modifiedArg := opener
113		for _, s := range strings.Split(flag.GetName(), ",") {
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