Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,36 @@ greet [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...]

````

## Available doc types

- `docs.ToMarkdown(cmd)`
- `docs.ToTabularMarkdown(cmd, appPath)`
- `docs.ToMan(cmd)`
- `docs.ToManWithSection(cmd, section)`

## Custom templates

The package ships with embedded templates in `MarkdownDocTemplate` and
`MarkdownTabularDocTemplate`, and it also lets you render with your own
template string or `.gotmpl` file via a call.

```go
customMarkdown, err := docs.TemplateFile("docs/custom.md.gotmpl").ToMarkdown(app)
if err != nil {
panic(err)
}

customTabular, err := docs.Template(
`# {{ .Name }}

{{ range .Commands }}- {{ .Name }}
{{ end }}`,
).ToTabularMarkdown(app, "greet")
if err != nil {
panic(err)
}
```

## Examples
Some examples of the cli generated using this markdown
* https://woodpecker-ci.org/docs/cli
95 changes: 87 additions & 8 deletions docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,44 @@ func tracef(format string, a ...any) {
)
}

// DocsBuilder renders documentation with a caller-provided template string or
// template file.
type DocsBuilder struct {
docTemplate string
err error
}

// Template returns a builder that renders documentation with the provided Go
// template string.
func Template(docTemplate string) DocsBuilder {
return DocsBuilder{docTemplate: docTemplate}
}

// TemplateFile returns a builder that renders documentation with the provided
// Go template file.
func TemplateFile(templatePath string) DocsBuilder {
docTemplate, err := readTemplateFile(templatePath)
return DocsBuilder{
docTemplate: docTemplate,
err: err,
}
}

// ToTabularMarkdown creates a tabular markdown documentation for
// the `*cli.Command`. The function errors if either parsing or
// writing of the string fails.
func ToTabularMarkdown(cmd *cli.Command, appPath string) (string, error) {
return Template(MarkdownTabularDocTemplate).ToTabularMarkdown(cmd, appPath)
}

// ToTabularMarkdown creates a tabular markdown documentation for
// the `*cli.Command` using the builder template. The function errors if either
// parsing or writing of the string fails.
func (b DocsBuilder) ToTabularMarkdown(cmd *cli.Command, appPath string) (string, error) {
if b.err != nil {
return "", b.err
}

if appPath == "" {
appPath = "app"
}
Expand All @@ -68,7 +102,7 @@ func ToTabularMarkdown(cmd *cli.Command, appPath string) (string, error) {

t, err := template.New(name).Funcs(template.FuncMap{
"join": strings.Join,
}).Parse(MarkdownTabularDocTemplate)
}).Parse(b.docTemplate)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -137,8 +171,19 @@ func ToTabularToFileBetweenTags(cmd *cli.Command, appPath, filePath string, star
// ToMarkdown creates a markdown string for the `*cli.Command`
// The function errors if either parsing or writing of the string fails.
func ToMarkdown(cmd *cli.Command) (string, error) {
return Template(MarkdownDocTemplate).ToMarkdown(cmd)
}

// ToMarkdown creates a markdown string for the `*cli.Command` using the
// builder template. The function errors if either parsing or writing of the
// string fails.
func (b DocsBuilder) ToMarkdown(cmd *cli.Command) (string, error) {
if b.err != nil {
return "", b.err
}

var w bytes.Buffer
if err := writeDocTemplate(cmd, &w, 0); err != nil {
if err := writeDocTemplate(cmd, &w, 0, b.docTemplate); err != nil {
return "", err
}
return w.String(), nil
Expand All @@ -148,8 +193,19 @@ func ToMarkdown(cmd *cli.Command) (string, error) {
// `*cli.Command` The function errors if either parsing or writing
// of the string fails.
func ToManWithSection(cmd *cli.Command, sectionNumber int) (string, error) {
return Template(MarkdownDocTemplate).ToManWithSection(cmd, sectionNumber)
}

// ToManWithSection creates a man page string with section number for the
// `*cli.Command` using the builder template. The function errors if either
// parsing or writing of the string fails.
func (b DocsBuilder) ToManWithSection(cmd *cli.Command, sectionNumber int) (string, error) {
if b.err != nil {
return "", b.err
}

var w bytes.Buffer
if err := writeDocTemplate(cmd, &w, sectionNumber); err != nil {
if err := writeDocTemplate(cmd, &w, sectionNumber, b.docTemplate); err != nil {
return "", err
}
man := md2man.Render(w.Bytes())
Expand All @@ -159,8 +215,14 @@ func ToManWithSection(cmd *cli.Command, sectionNumber int) (string, error) {
// ToMan creates a man page string for the `*cli.Command`
// The function errors if either parsing or writing of the string fails.
func ToMan(cmd *cli.Command) (string, error) {
man, err := ToManWithSection(cmd, 8)
return man, err
return ToManWithSection(cmd, 8)
}

// ToMan creates a man page string for the `*cli.Command` using the builder
// template. The function errors if either parsing or writing of the string
// fails.
func (b DocsBuilder) ToMan(cmd *cli.Command) (string, error) {
return b.ToManWithSection(cmd, 8)
}

type cliCommandTemplate struct {
Expand All @@ -171,11 +233,11 @@ type cliCommandTemplate struct {
SynopsisArgs []string
}

func writeDocTemplate(cmd *cli.Command, w io.Writer, sectionNum int) error {
tracef("using MarkdownDocTemplate starting %[1]q", string([]byte(MarkdownDocTemplate)[0:8]))
func writeDocTemplate(cmd *cli.Command, w io.Writer, sectionNum int, docTemplate string) error {
tracef("using MarkdownDocTemplate starting %[1]q", previewTemplate(docTemplate))

const name = "cli"
t, err := template.New(name).Parse(MarkdownDocTemplate)
t, err := template.New(name).Parse(docTemplate)
if err != nil {
return err
}
Expand All @@ -189,6 +251,23 @@ func writeDocTemplate(cmd *cli.Command, w io.Writer, sectionNum int) error {
})
}

func readTemplateFile(templatePath string) (string, error) {
data, err := os.ReadFile(templatePath)
if err != nil {
return "", err
}

return string(data), nil
}

func previewTemplate(docTemplate string) string {
if len(docTemplate) <= 8 {
return docTemplate
}

return docTemplate[:8]
}

func prepareCommands(commands []*cli.Command, level int) []string {
var coms []string
for _, command := range commands {
Expand Down
94 changes: 94 additions & 0 deletions docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/cpuguy83/go-md2man/v2/md2man"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
Expand Down Expand Up @@ -190,6 +191,32 @@ func TestToMarkdownFull(t *testing.T) {
expectFileContent(t, "testdata/expected-doc-full.md", res)
}

func TestBuilderToMarkdown(t *testing.T) {
cmd := buildExtendedTestCommand(t)

res, err := Template(`{{ .Command.Name }}|{{ .SectionNum }}|{{ len .Commands }}|{{ len .GlobalArgs }}|{{ len .SynopsisArgs }}`).ToMarkdown(cmd)

require.NoError(t, err)
require.Equal(t, "greet|0|6|4|4", res)
}

func TestBuilderToMarkdownFromFile(t *testing.T) {
cmd := buildExtendedTestCommand(t)

tmpFile, err := os.CreateTemp("", "*.gotmpl")
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile.Name()) })

_, err = tmpFile.WriteString(`{{ .Command.Name }} from {{ .Command.Usage }}`)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())

res, err := TemplateFile(tmpFile.Name()).ToMarkdown(cmd)

require.NoError(t, err)
require.Equal(t, "greet from Some app", res)
}

func TestToTabularMarkdown(t *testing.T) {
app := buildExtendedTestCommand(t)

Expand All @@ -212,6 +239,32 @@ func TestToTabularMarkdown(t *testing.T) {
})
}

func TestBuilderToTabularMarkdown(t *testing.T) {
app := buildExtendedTestCommand(t)

res, err := Template(`{{ .AppPath }}|{{ .Name }}|{{ join (index .Commands 0).Aliases "," }}`).ToTabularMarkdown(app, "/usr/local/bin")

require.NoError(t, err)
require.Equal(t, "/usr/local/bin|greet|c\n", res)
}

func TestBuilderToTabularMarkdownFromFile(t *testing.T) {
app := buildExtendedTestCommand(t)

tmpFile, err := os.CreateTemp("", "*.gotmpl")
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile.Name()) })

_, err = tmpFile.WriteString(`{{ .AppPath }}|{{ (index .Commands 1).Name }}`)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())

res, err := TemplateFile(tmpFile.Name()).ToTabularMarkdown(app, "/usr/local/bin")

require.NoError(t, err)
require.Equal(t, "/usr/local/bin|info\n", res)
}

func TestToTabularMarkdownFailed(t *testing.T) {
tpl := MarkdownTabularDocTemplate
t.Cleanup(func() { MarkdownTabularDocTemplate = tpl })
Expand Down Expand Up @@ -389,6 +442,19 @@ func TestToMan(t *testing.T) {
expectFileContent(t, "testdata/expected-doc-full.man", res)
}

func TestBuilderToMan(t *testing.T) {
app := buildExtendedTestCommand(t)
tpl := `# NAME

{{ .Command.Name }}
`

res, err := Template(tpl).ToMan(app)

require.NoError(t, err)
require.Equal(t, string(md2man.Render([]byte("# NAME\n\ngreet\n"))), res)
}

func TestToManParseError(t *testing.T) {
app := buildExtendedTestCommand(t)

Expand All @@ -401,6 +467,34 @@ func TestToManParseError(t *testing.T) {
require.ErrorContains(t, err, "template: cli:1: unclosed action")
}

func TestTemplateFileMissing(t *testing.T) {
app := buildExtendedTestCommand(t)

_, err := TemplateFile("/missing/template.gotmpl").ToMarkdown(app)

require.ErrorIs(t, err, fs.ErrNotExist)
}

func TestBuilderToManWithSectionFromFile(t *testing.T) {
app := buildExtendedTestCommand(t)

tmpFile, err := os.CreateTemp("", "*.gotmpl")
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile.Name()) })

_, err = tmpFile.WriteString(`# NAME

{{ .Command.Name }} ({{ .SectionNum }})
`)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())

res, err := TemplateFile(tmpFile.Name()).ToManWithSection(app, 5)

require.NoError(t, err)
require.Equal(t, string(md2man.Render([]byte("# NAME\n\ngreet (5)\n"))), res)
}

func TestToManWithSection(t *testing.T) {
cmd := buildExtendedTestCommand(t)

Expand Down
Loading