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 }} &lt;{{.Varname}}&gt;{{ 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  }