From 0d783c754bfc59f1563b7c32448e42884b2c0cf0 Mon Sep 17 00:00:00 2001 From: Jeff Principe Date: Wed, 5 Jan 2022 21:23:52 -0800 Subject: [PATCH] feat(cmd) add embed option (#59) --- README.md | 23 +++++ cmd/gomarkdoc/command.go | 110 ++------------------- cmd/gomarkdoc/command_test.go | 27 ++++++ cmd/gomarkdoc/output.go | 156 ++++++++++++++++++++++++++++++ doc.go | 24 +++++ testData/embed/.gomarkdoc.yml | 2 + testData/embed/README-template.md | 22 +++++ testData/embed/README.md | 103 ++++++++++++++++++++ testData/embed/embed.go | 8 ++ 9 files changed, 375 insertions(+), 100 deletions(-) create mode 100644 cmd/gomarkdoc/output.go create mode 100644 testData/embed/.gomarkdoc.yml create mode 100644 testData/embed/README-template.md create mode 100644 testData/embed/README.md create mode 100644 testData/embed/embed.go diff --git a/README.md b/README.md index 373d3e5..795e346 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Usage: Flags: -c, --check Check the output to see if it matches the generated documentation. --output must be specified to use this. --config string File from which to load configuration (default: .gomarkdoc.yml) + -e, --embed Embed documentation into existing markdown files if available, otherwise append to file. --footer string Additional content to inject at the end of each output file. --footer-file string File containing additional content to inject at the end of each output file. -f, --format string Format to use for writing output data. Valid options: github (default), azure-devops, plain (default "github") @@ -133,6 +134,28 @@ As with the godoc tool itself\, only exported symbols will be shown in documenta gomarkdoc -u -o README.md . ``` +If you want to blend the documentation generated by gomarkdoc with your own hand\-written markdown\, you can use the \-\-embed/\-e flag to change the gomarkdoc tool into an append/embed mode\. When documentation is generated\, gomarkdoc looks for a file in the location where the documentation is to be written and embeds the documentation if present\. Otherwise\, the documentation is appended to the end of the file\. + +``` +gomarkdoc -o README.md -e . +``` + +When running with embed mode enabled\, gomarkdoc will look for either this single comment: + +``` + +``` + +Or the following pair of comments \(in which case all content in between is replaced\): + +``` + + +This content is replaced with the embedded documentation + + +``` + If you would like to include files that are part of a build tag\, you can specify build tags with the \-\-tags flag\. Tags are also supported through GOFLAGS\, though command line and configuration file definitions override tags specified through GOFLAGS\. ``` diff --git a/cmd/gomarkdoc/command.go b/cmd/gomarkdoc/command.go index b93f2ee..fc3f250 100644 --- a/cmd/gomarkdoc/command.go +++ b/cmd/gomarkdoc/command.go @@ -57,6 +57,7 @@ type commandOptions struct { verbosity int includeUnexported bool check bool + embed bool version bool } @@ -86,6 +87,7 @@ func buildCommand() *cobra.Command { opts.includeUnexported = viper.GetBool("includeUnexported") opts.output = viper.GetString("output") opts.check = viper.GetBool("check") + opts.embed = viper.GetBool("embed") opts.format = viper.GetString("format") opts.templateOverrides = viper.GetStringMapString("template") opts.templateFileOverrides = viper.GetStringMapString("templateFile") @@ -138,6 +140,13 @@ func buildCommand() *cobra.Command { false, "Check the output to see if it matches the generated documentation. --output must be specified to use this.", ) + command.Flags().BoolVarP( + &opts.embed, + "embed", + "e", + false, + "Embed documentation into existing markdown files if available, otherwise append to file.", + ) command.Flags().StringVarP( &opts.format, "format", @@ -223,6 +232,7 @@ func buildCommand() *cobra.Command { _ = viper.BindPFlag("includeUnexported", command.Flags().Lookup("include-unexported")) _ = viper.BindPFlag("output", command.Flags().Lookup("output")) _ = viper.BindPFlag("check", command.Flags().Lookup("check")) + _ = viper.BindPFlag("embed", command.Flags().Lookup("embed")) _ = viper.BindPFlag("format", command.Flags().Lookup("format")) _ = viper.BindPFlag("template", command.Flags().Lookup("template")) _ = viper.BindPFlag("templateFile", command.Flags().Lookup("template-file")) @@ -422,106 +432,6 @@ func loadPackages(specs []*PackageSpec, opts commandOptions) error { return nil } -func writeOutput(specs []*PackageSpec, opts commandOptions) error { - overrides, err := resolveOverrides(opts) - if err != nil { - return err - } - - out, err := gomarkdoc.NewRenderer(overrides...) - if err != nil { - return err - } - - header, err := resolveHeader(opts) - if err != nil { - return err - } - - footer, err := resolveFooter(opts) - if err != nil { - return err - } - - filePkgs := make(map[string][]*lang.Package) - - for _, spec := range specs { - if spec.pkg == nil { - continue - } - - filePkgs[spec.outputFile] = append(filePkgs[spec.outputFile], spec.pkg) - } - - for fileName, pkgs := range filePkgs { - file := lang.NewFile(header, footer, pkgs) - - text, err := out.File(file) - if err != nil { - return err - } - - switch { - case fileName == "": - fmt.Fprint(os.Stdout, text) - case opts.check: - var b bytes.Buffer - fmt.Fprint(&b, text) - if err := checkFile(&b, fileName); err != nil { - return err - } - default: - if err := writeFile(fileName, text); err != nil { - return fmt.Errorf("failed to write output file %s: %w", fileName, err) - } - } - } - - return nil -} - -func writeFile(fileName string, text string) error { - folder := filepath.Dir(fileName) - - if folder != "" { - if err := os.MkdirAll(folder, 0755); err != nil { - return fmt.Errorf("failed to create folder %s: %w", folder, err) - } - } - - if err := ioutil.WriteFile(fileName, []byte(text), 0755); err != nil { - return fmt.Errorf("failed to write file %s: %w", fileName, err) - } - - return nil -} - -func checkFile(b *bytes.Buffer, path string) error { - checkErr := errors.New("output does not match current files. Did you forget to run gomarkdoc?") - - f, err := os.Open(path) - if err != nil { - if err == os.ErrNotExist { - return checkErr - } - - return fmt.Errorf("failed to open file %s for checking: %w", path, err) - } - - defer f.Close() - - match, err := compare(b, f) - if err != nil { - return fmt.Errorf("failure while attempting to check contents of %s: %w", path, err) - } - - if !match { - return checkErr - } - - return nil -} - func getBuildPackage(path string, tags []string) (*build.Package, error) { ctx := build.Default ctx.BuildTags = tags diff --git a/cmd/gomarkdoc/command_test.go b/cmd/gomarkdoc/command_test.go index 3ee2370..97f66b5 100644 --- a/cmd/gomarkdoc/command_test.go +++ b/cmd/gomarkdoc/command_test.go @@ -279,6 +279,33 @@ func TestCommand_tagsWithGOFLAGSNoParse(t *testing.T) { verifyNotEqual(t, "./tags") } +func TestCommand_embed(t *testing.T) { + is := is.New(t) + + err := os.Chdir(filepath.Join(wd, "../../testData")) + is.NoErr(err) + + os.Args = []string{ + "gomarkdoc", "./embed", + "--embed", + "-o", "{{.Dir}}/README-test.md", + "--repository.url", "https://github.com/princjef/gomarkdoc", + "--repository.default-branch", "master", + "--repository.path", "/testData/", + } + cleanup("embed") + + data, err := os.ReadFile("./embed/README-template.md") + is.NoErr(err) + + err = os.WriteFile("./embed/README-test.md", data, 0644) + is.NoErr(err) + + main() + + verify(t, "./embed") +} + func TestCommand_untagged(t *testing.T) { is := is.New(t) diff --git a/cmd/gomarkdoc/output.go b/cmd/gomarkdoc/output.go new file mode 100644 index 0000000..f910f18 --- /dev/null +++ b/cmd/gomarkdoc/output.go @@ -0,0 +1,156 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + + "github.com/princjef/gomarkdoc" + "github.com/princjef/gomarkdoc/lang" + "github.com/princjef/gomarkdoc/logger" +) + +func writeOutput(specs []*PackageSpec, opts commandOptions) error { + log := logger.New(getLogLevel(opts.verbosity)) + + overrides, err := resolveOverrides(opts) + if err != nil { + return err + } + + out, err := gomarkdoc.NewRenderer(overrides...) + if err != nil { + return err + } + + header, err := resolveHeader(opts) + if err != nil { + return err + } + + footer, err := resolveFooter(opts) + if err != nil { + return err + } + + filePkgs := make(map[string][]*lang.Package) + + for _, spec := range specs { + if spec.pkg == nil { + continue + } + + filePkgs[spec.outputFile] = append(filePkgs[spec.outputFile], spec.pkg) + } + + for fileName, pkgs := range filePkgs { + file := lang.NewFile(header, footer, pkgs) + + text, err := out.File(file) + if err != nil { + return err + } + + switch { + case fileName == "": + fmt.Fprint(os.Stdout, text) + case opts.check: + var b bytes.Buffer + fmt.Fprint(&b, text) + if err := checkFile(&b, fileName); err != nil { + return err + } + default: + if opts.embed { + text = embedContents(log, fileName, text) + } + + if err := writeFile(fileName, text); err != nil { + return fmt.Errorf("failed to write output file %s: %w", fileName, err) + } + } + } + + return nil +} + +func writeFile(fileName string, text string) error { + folder := filepath.Dir(fileName) + + if folder != "" { + if err := os.MkdirAll(folder, 0755); err != nil { + return fmt.Errorf("failed to create folder %s: %w", folder, err) + } + } + + if err := ioutil.WriteFile(fileName, []byte(text), 0755); err != nil { + return fmt.Errorf("failed to write file %s: %w", fileName, err) + } + + return nil +} + +func checkFile(b *bytes.Buffer, path string) error { + checkErr := errors.New("output does not match current files. Did you forget to run gomarkdoc?") + + f, err := os.Open(path) + if err != nil { + if err == os.ErrNotExist { + return checkErr + } + + return fmt.Errorf("failed to open file %s for checking: %w", path, err) + } + + defer f.Close() + + match, err := compare(b, f) + if err != nil { + return fmt.Errorf("failure while attempting to check contents of %s: %w", path, err) + } + + if !match { + return checkErr + } + + return nil +} + +var ( + embedStandaloneRegex = regexp.MustCompile(`(?m:^ *)(?m:\s*?$)`) + embedStartRegex = regexp.MustCompile( + `(?m:^ *)(?s:.*?)(?m:\s*?$)`, + ) +) + +func embedContents(log logger.Logger, fileName string, text string) string { + embedText := fmt.Sprintf("\n\n%s\n\n", text) + + data, err := os.ReadFile(fileName) + if err != nil { + log.Debugf("unable to find output file %s for embedding. Creating a new file instead", fileName) + return embedText + } + + var replacements int + data = embedStandaloneRegex.ReplaceAllFunc(data, func(_ []byte) []byte { + replacements++ + return []byte(embedText) + }) + + data = embedStartRegex.ReplaceAllFunc(data, func(_ []byte) []byte { + replacements++ + return []byte(embedText) + }) + + if replacements == 0 { + log.Debugf("no embed markers found. Appending documentation to the end of the file instead") + return fmt.Sprintf("%s\n\n%s", string(data), text) + } + + return string(data) +} diff --git a/doc.go b/doc.go index 3dd1a2f..11b19dc 100644 --- a/doc.go +++ b/doc.go @@ -27,6 +27,7 @@ // Flags: // -c, --check Check the output to see if it matches the generated documentation. --output must be specified to use this. // --config string File from which to load configuration (default: .gomarkdoc.yml) +// -e, --embed Embed documentation into existing markdown files if available, otherwise append to file. // --footer string Additional content to inject at the end of each output file. // --footer-file string File containing additional content to inject at the end of each output file. // -f, --format string Format to use for writing output data. Valid options: github (default), azure-devops, plain (default "github") @@ -138,6 +139,29 @@ // // gomarkdoc -u -o README.md . // +// If you want to blend the documentation generated by gomarkdoc with your own +// hand-written markdown, you can use the --embed/-e flag to change the +// gomarkdoc tool into an append/embed mode. When documentation is generated, +// gomarkdoc looks for a file in the location where the documentation is to be +// written and embeds the documentation if present. Otherwise, the documentation +// is appended to the end of the file. +// +// gomarkdoc -o README.md -e . +// +// When running with embed mode enabled, gomarkdoc will look for either this +// single comment: +// +// +// +// Or the following pair of comments (in which case all content in between is +// replaced): +// +// +// +// This content is replaced with the embedded documentation +// +// +// // If you would like to include files that are part of a build tag, you can // specify build tags with the --tags flag. Tags are also supported through // GOFLAGS, though command line and configuration file definitions override tags diff --git a/testData/embed/.gomarkdoc.yml b/testData/embed/.gomarkdoc.yml new file mode 100644 index 0000000..bdf3729 --- /dev/null +++ b/testData/embed/.gomarkdoc.yml @@ -0,0 +1,2 @@ +output: "{{.Dir}}/README.md" +embed: true \ No newline at end of file diff --git a/testData/embed/README-template.md b/testData/embed/README-template.md new file mode 100644 index 0000000..3185fc8 --- /dev/null +++ b/testData/embed/README-template.md @@ -0,0 +1,22 @@ +This is content before the embed + + + +This is content after the embed + + + +This is content after the second embed + + + +This content will be replaced with the embed + + + +This is content after the third embed \ No newline at end of file diff --git a/testData/embed/README.md b/testData/embed/README.md new file mode 100644 index 0000000..73aeb77 --- /dev/null +++ b/testData/embed/README.md @@ -0,0 +1,103 @@ +This is content before the embed + + + + + +# embed + +```go +import "github.com/princjef/gomarkdoc/testData/embed" +``` + +Package embed tests out embedding of documentation in an existing readme\. + +## Index + +- [func EmbeddedFunc(param int) int](<#func-embeddedfunc>) + + +## func [EmbeddedFunc]() + +```go +func EmbeddedFunc(param int) int +``` + +EmbeddedFunc is present in embedded content\. + + + +Generated by [gomarkdoc]() + + + + +This is content after the embed + + + + + +# embed + +```go +import "github.com/princjef/gomarkdoc/testData/embed" +``` + +Package embed tests out embedding of documentation in an existing readme\. + +## Index + +- [func EmbeddedFunc(param int) int](<#func-embeddedfunc>) + + +## func [EmbeddedFunc]() + +```go +func EmbeddedFunc(param int) int +``` + +EmbeddedFunc is present in embedded content\. + + + +Generated by [gomarkdoc]() + + + + +This is content after the second embed + + + + + +# embed + +```go +import "github.com/princjef/gomarkdoc/testData/embed" +``` + +Package embed tests out embedding of documentation in an existing readme\. + +## Index + +- [func EmbeddedFunc(param int) int](<#func-embeddedfunc>) + + +## func [EmbeddedFunc]() + +```go +func EmbeddedFunc(param int) int +``` + +EmbeddedFunc is present in embedded content\. + + + +Generated by [gomarkdoc]() + + + + +This is content after the third embed \ No newline at end of file diff --git a/testData/embed/embed.go b/testData/embed/embed.go new file mode 100644 index 0000000..d6b24bf --- /dev/null +++ b/testData/embed/embed.go @@ -0,0 +1,8 @@ +// Package embed tests out embedding of documentation in an +// existing readme. +package embed + +// EmbeddedFunc is present in embedded content. +func EmbeddedFunc(param int) int { + return param +}