diff --git a/README.md b/README.md index ba3d4fb..6609c60 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs.go b/docs.go index 3d72121..5e8c9ce 100644 --- a/docs.go +++ b/docs.go @@ -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" } @@ -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 } @@ -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 @@ -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()) @@ -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 { @@ -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 } @@ -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 { diff --git a/docs_test.go b/docs_test.go index cdad29b..36ee298 100644 --- a/docs_test.go +++ b/docs_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/cpuguy83/go-md2man/v2/md2man" "github.com/stretchr/testify/require" "github.com/urfave/cli/v3" ) @@ -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) @@ -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 }) @@ -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) @@ -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)