github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/internal/docs/markdown.go (about) 1 package docs 2 3 import ( 4 "fmt" 5 "html/template" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/spf13/cobra" 12 "github.com/spf13/pflag" 13 ) 14 15 func printOptions(w io.Writer, cmd *cobra.Command) error { 16 flags := cmd.NonInheritedFlags() 17 flags.SetOutput(w) 18 if flags.HasAvailableFlags() { 19 fmt.Fprint(w, "### Options\n\n") 20 if err := printFlagsHTML(w, flags); err != nil { 21 return err 22 } 23 fmt.Fprint(w, "\n\n") 24 } 25 26 parentFlags := cmd.InheritedFlags() 27 parentFlags.SetOutput(w) 28 if hasNonHelpFlags(parentFlags) { 29 fmt.Fprint(w, "### Options inherited from parent commands\n\n") 30 if err := printFlagsHTML(w, parentFlags); err != nil { 31 return err 32 } 33 fmt.Fprint(w, "\n\n") 34 } 35 return nil 36 } 37 38 func hasNonHelpFlags(fs *pflag.FlagSet) (found bool) { 39 fs.VisitAll(func(f *pflag.Flag) { 40 if !f.Hidden && f.Name != "help" { 41 found = true 42 } 43 }) 44 return 45 } 46 47 type flagView struct { 48 Name string 49 Varname string 50 Shorthand string 51 Usage string 52 } 53 54 var flagsTemplate = ` 55 <dl class="flags">{{ range . }} 56 <dt>{{ if .Shorthand }}<code>-{{.Shorthand}}</code>, {{ end -}} 57 <code>--{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}</code></dt> 58 <dd>{{.Usage}}</dd> 59 {{ end }}</dl> 60 ` 61 62 var tpl = template.Must(template.New("flags").Parse(flagsTemplate)) 63 64 func printFlagsHTML(w io.Writer, fs *pflag.FlagSet) error { 65 var flags []flagView 66 fs.VisitAll(func(f *pflag.Flag) { 67 if f.Hidden || f.Name == "help" { 68 return 69 } 70 varname, usage := pflag.UnquoteUsage(f) 71 flags = append(flags, flagView{ 72 Name: f.Name, 73 Varname: varname, 74 Shorthand: f.Shorthand, 75 Usage: usage, 76 }) 77 }) 78 return tpl.Execute(w, flags) 79 } 80 81 // GenMarkdown creates markdown output. 82 func GenMarkdown(cmd *cobra.Command, w io.Writer) error { 83 return GenMarkdownCustom(cmd, w, func(s string) string { return s }) 84 } 85 86 // GenMarkdownCustom creates custom markdown output. 87 func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { 88 fmt.Fprint(w, "{% raw %}") 89 fmt.Fprintf(w, "## %s\n\n", cmd.CommandPath()) 90 91 hasLong := cmd.Long != "" 92 if !hasLong { 93 fmt.Fprintf(w, "%s\n\n", cmd.Short) 94 } 95 if cmd.Runnable() { 96 fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) 97 } 98 if hasLong { 99 fmt.Fprintf(w, "%s\n\n", cmd.Long) 100 } 101 102 for _, g := range subcommandGroups(cmd) { 103 if len(g.Commands) == 0 { 104 continue 105 } 106 fmt.Fprintf(w, "### %s\n\n", g.Name) 107 for _, subcmd := range g.Commands { 108 fmt.Fprintf(w, "* [%s](%s)\n", subcmd.CommandPath(), linkHandler(cmdManualPath(subcmd))) 109 } 110 fmt.Fprint(w, "\n\n") 111 } 112 113 if err := printOptions(w, cmd); err != nil { 114 return err 115 } 116 fmt.Fprint(w, "{% endraw %}\n") 117 118 if len(cmd.Example) > 0 { 119 fmt.Fprint(w, "### Examples\n\n{% highlight bash %}{% raw %}\n") 120 fmt.Fprint(w, cmd.Example) 121 fmt.Fprint(w, "{% endraw %}{% endhighlight %}\n\n") 122 } 123 124 if cmd.HasParent() { 125 p := cmd.Parent() 126 fmt.Fprint(w, "### See also\n\n") 127 fmt.Fprintf(w, "* [%s](%s)\n", p.CommandPath(), linkHandler(cmdManualPath(p))) 128 } 129 130 return nil 131 } 132 133 type commandGroup struct { 134 Name string 135 Commands []*cobra.Command 136 } 137 138 // subcommandGroups lists child commands of a Cobra command split into groups. 139 // TODO: have rootHelpFunc use this instead of repeating the same logic. 140 func subcommandGroups(c *cobra.Command) []commandGroup { 141 var rest []*cobra.Command 142 var core []*cobra.Command 143 var actions []*cobra.Command 144 145 for _, subcmd := range c.Commands() { 146 if !subcmd.IsAvailableCommand() { 147 continue 148 } 149 if _, ok := subcmd.Annotations["IsCore"]; ok { 150 core = append(core, subcmd) 151 } else if _, ok := subcmd.Annotations["IsActions"]; ok { 152 actions = append(actions, subcmd) 153 } else { 154 rest = append(rest, subcmd) 155 } 156 } 157 158 if len(core) > 0 { 159 return []commandGroup{ 160 { 161 Name: "Core commands", 162 Commands: core, 163 }, 164 { 165 Name: "Actions commands", 166 Commands: actions, 167 }, 168 { 169 Name: "Additional commands", 170 Commands: rest, 171 }, 172 } 173 } 174 175 return []commandGroup{ 176 { 177 Name: "Commands", 178 Commands: rest, 179 }, 180 } 181 } 182 183 // GenMarkdownTree will generate a markdown page for this command and all 184 // descendants in the directory given. The header may be nil. 185 // This function may not work correctly if your command names have `-` in them. 186 // If you have `cmd` with two subcmds, `sub` and `sub-third`, 187 // and `sub` has a subcommand called `third`, it is undefined which 188 // help output will be in the file `cmd-sub-third.1`. 189 func GenMarkdownTree(cmd *cobra.Command, dir string) error { 190 identity := func(s string) string { return s } 191 emptyStr := func(s string) string { return "" } 192 return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) 193 } 194 195 // GenMarkdownTreeCustom is the same as GenMarkdownTree, but 196 // with custom filePrepender and linkHandler. 197 func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { 198 for _, c := range cmd.Commands() { 199 _, forceGeneration := c.Annotations["markdown:generate"] 200 if c.Hidden && !forceGeneration { 201 continue 202 } 203 204 if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil { 205 return err 206 } 207 } 208 209 filename := filepath.Join(dir, cmdManualPath(cmd)) 210 f, err := os.Create(filename) 211 if err != nil { 212 return err 213 } 214 defer f.Close() 215 216 if _, err := io.WriteString(f, filePrepender(filename)); err != nil { 217 return err 218 } 219 if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil { 220 return err 221 } 222 return nil 223 } 224 225 func cmdManualPath(c *cobra.Command) string { 226 if basenameOverride, found := c.Annotations["markdown:basename"]; found { 227 return basenameOverride + ".md" 228 } 229 return strings.ReplaceAll(c.CommandPath(), " ", "_") + ".md" 230 }