Skip to content

Commit

Permalink
feat(cmd) add embed option (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
princjef committed Jan 6, 2022
1 parent 08e0e91 commit 0d783c7
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 100 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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:

```
<!-- gomarkdoc:embed -->
```

Or the following pair of comments \(in which case all content in between is replaced\):

```
<!-- gomarkdoc:embed:start -->
This content is replaced with the embedded documentation
<!-- gomarkdoc:embed:end -->
```

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\.

```
Expand Down
110 changes: 10 additions & 100 deletions cmd/gomarkdoc/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type commandOptions struct {
verbosity int
includeUnexported bool
check bool
embed bool
version bool
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions cmd/gomarkdoc/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
156 changes: 156 additions & 0 deletions cmd/gomarkdoc/output.go
Original file line number Diff line number Diff line change
@@ -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:^ *)<!--\s*gomarkdoc:embed\s*-->(?m:\s*?$)`)
embedStartRegex = regexp.MustCompile(
`(?m:^ *)<!--\s*gomarkdoc:embed:start\s*-->(?s:.*?)<!--\s*gomarkdoc:embed:end\s*-->(?m:\s*?$)`,
)
)

func embedContents(log logger.Logger, fileName string, text string) string {
embedText := fmt.Sprintf("<!-- gomarkdoc:embed:start -->\n\n%s\n\n<!-- gomarkdoc:embed:end -->", 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)
}
Loading

0 comments on commit 0d783c7

Please sign in to comment.