Skip to content

Commit

Permalink
feat(format): allow template funcs to be customized (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
princjef authored Jun 12, 2023
1 parent 7f25ccf commit 0124a63
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 76 deletions.
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,12 @@ Know of another project that is using gomarkdoc? Open an issue with a descriptio
- [func \(out \*Renderer\) Type\(typ \*lang.Type\) \(string, error\)](<#Renderer.Type>)
- [type RendererOption](<#RendererOption>)
- [func WithFormat\(format format.Format\) RendererOption](<#WithFormat>)
- [func WithTemplateFunc\(name string, fn any\) RendererOption](<#WithTemplateFunc>)
- [func WithTemplateOverride\(name, tmpl string\) RendererOption](<#WithTemplateOverride>)


<a name="Renderer"></a>
## type [Renderer](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L16-L20>)
## type [Renderer](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L16-L21>)

Renderer provides capabilities for rendering various types of documentation with the configured format and templates.

Expand All @@ -253,7 +254,7 @@ type Renderer struct {
```

<a name="NewRenderer"></a>
### func [NewRenderer](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L31>)
### func [NewRenderer](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L32>)

```go
func NewRenderer(opts ...RendererOption) (*Renderer, error)
Expand All @@ -262,7 +263,7 @@ func NewRenderer(opts ...RendererOption) (*Renderer, error)
NewRenderer initializes a Renderer configured using the provided options. If nothing special is provided, the created renderer will use the default set of templates and the GitHubFlavoredMarkdown.

<a name="Renderer.Example"></a>
### func \(\*Renderer\) [Example](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L184>)
### func \(\*Renderer\) [Example](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L135>)

```go
func (out *Renderer) Example(ex *lang.Example) (string, error)
Expand All @@ -271,7 +272,7 @@ func (out *Renderer) Example(ex *lang.Example) (string, error)
Example renders an example's documentation to a string. You can change the rendering of the example by overriding the "example" template or one of the templates it references.

<a name="Renderer.File"></a>
### func \(\*Renderer\) [File](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L156>)
### func \(\*Renderer\) [File](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L107>)

```go
func (out *Renderer) File(file *lang.File) (string, error)
Expand All @@ -280,7 +281,7 @@ func (out *Renderer) File(file *lang.File) (string, error)
File renders a file containing one or more packages to document to a string. You can change the rendering of the file by overriding the "file" template or one of the templates it references.

<a name="Renderer.Func"></a>
### func \(\*Renderer\) [Func](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L170>)
### func \(\*Renderer\) [Func](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L121>)

```go
func (out *Renderer) Func(fn *lang.Func) (string, error)
Expand All @@ -289,7 +290,7 @@ func (out *Renderer) Func(fn *lang.Func) (string, error)
Func renders a function's documentation to a string. You can change the rendering of the package by overriding the "func" template or one of the templates it references.

<a name="Renderer.Package"></a>
### func \(\*Renderer\) [Package](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L163>)
### func \(\*Renderer\) [Package](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L114>)

```go
func (out *Renderer) Package(pkg *lang.Package) (string, error)
Expand All @@ -298,7 +299,7 @@ func (out *Renderer) Package(pkg *lang.Package) (string, error)
Package renders a package's documentation to a string. You can change the rendering of the package by overriding the "package" template or one of the templates it references.

<a name="Renderer.Type"></a>
### func \(\*Renderer\) [Type](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L177>)
### func \(\*Renderer\) [Type](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L128>)

```go
func (out *Renderer) Type(typ *lang.Type) (string, error)
Expand All @@ -307,7 +308,7 @@ func (out *Renderer) Type(typ *lang.Type) (string, error)
Type renders a type's documentation to a string. You can change the rendering of the type by overriding the "type" template or one of the templates it references.

<a name="RendererOption"></a>
## type [RendererOption](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L23>)
## type [RendererOption](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L24>)

RendererOption configures the renderer's behavior.

Expand All @@ -316,16 +317,27 @@ type RendererOption func(renderer *Renderer) error
```

<a name="WithFormat"></a>
### func [WithFormat](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L146>)
### func [WithFormat](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L83>)

```go
func WithFormat(format format.Format) RendererOption
```

WithFormat changes the renderer to use the format provided instead of the default format.

<a name="WithTemplateFunc"></a>
### func [WithTemplateFunc](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L97>)

```go
func WithTemplateFunc(name string, fn any) RendererOption
```

WithTemplateFunc adds the provided function with the given name to the list of functions that can be used by the rendering templates.

Any name collisions between built\-in functions and functions provided here are resolved in favor of the function provided here, so be careful about the naming of your functions to avoid overriding existing behavior unless desired.

<a name="WithTemplateOverride"></a>
### func [WithTemplateOverride](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L132>)
### func [WithTemplateOverride](<https://github.com/princjef/gomarkdoc/blob/master/renderer.go#L69>)

```go
func WithTemplateOverride(name, tmpl string) RendererOption
Expand Down
162 changes: 96 additions & 66 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type (
templateOverrides map[string]string
tmpl *template.Template
format format.Format
templateFuncs map[string]any
}

// RendererOption configures the renderer's behavior.
Expand All @@ -32,6 +33,7 @@ func NewRenderer(opts ...RendererOption) (*Renderer, error) {
renderer := &Renderer{
templateOverrides: make(map[string]string),
format: &format.GitHubFlavoredMarkdown{},
templateFuncs: map[string]any{},
}

for _, opt := range opts {
Expand All @@ -47,72 +49,7 @@ func NewRenderer(opts ...RendererOption) (*Renderer, error) {
}

if renderer.tmpl == nil {
tmpl := template.New(name)
tmpl.Funcs(map[string]interface{}{
"add": func(n1, n2 int) int {
return n1 + n2
},
"spacer": func() string {
return "\n\n"
},
"inlineSpacer": func() string {
return "\n"
},
"hangingIndent": func(s string, n int) string {
return strings.ReplaceAll(s, "\n", fmt.Sprintf("\n%s", strings.Repeat(" ", n)))
},
"include": func(name string, data any) (string, error) {
var b strings.Builder
err := tmpl.ExecuteTemplate(&b, name, data)
if err != nil {
return "", err
}

return b.String(), nil
},
"iter": func(l any) (any, error) {
type iter struct {
First bool
Last bool
Entry any
}

switch reflect.TypeOf(l).Kind() {
case reflect.Slice:
s := reflect.ValueOf(l)
out := make([]iter, s.Len())

for i := 0; i < s.Len(); i++ {
out[i] = iter{
First: i == 0,
Last: i == s.Len()-1,
Entry: s.Index(i).Interface(),
}
}

return out, nil
default:
return nil, fmt.Errorf("renderer: iter only accepts slices")
}
},

"bold": renderer.format.Bold,
"anchor": renderer.format.Anchor,
"anchorHeader": renderer.format.AnchorHeader,
"header": renderer.format.Header,
"rawAnchorHeader": renderer.format.RawAnchorHeader,
"rawHeader": renderer.format.RawHeader,
"codeBlock": renderer.format.CodeBlock,
"link": renderer.format.Link,
"listEntry": renderer.format.ListEntry,
"accordion": renderer.format.Accordion,
"accordionHeader": renderer.format.AccordionHeader,
"accordionTerminator": renderer.format.AccordionTerminator,
"localHref": renderer.format.LocalHref,
"rawLocalHref": renderer.format.RawLocalHref,
"codeHref": renderer.format.CodeHref,
"escape": renderer.format.Escape,
})
tmpl := renderer.getTemplate(name)

if _, err := tmpl.Parse(tmplStr); err != nil {
return nil, err
Expand Down Expand Up @@ -150,6 +87,20 @@ func WithFormat(format format.Format) RendererOption {
}
}

// WithTemplateFunc adds the provided function with the given name to the list
// of functions that can be used by the rendering templates.
//
// Any name collisions between built-in functions and functions provided here
// are resolved in favor of the function provided here, so be careful about the
// naming of your functions to avoid overriding existing behavior unless
// desired.
func WithTemplateFunc(name string, fn any) RendererOption {
return func(renderer *Renderer) error {
renderer.templateFuncs[name] = fn
return nil
}
}

// File renders a file containing one or more packages to document to a string.
// You can change the rendering of the file by overriding the "file" template
// or one of the templates it references.
Expand Down Expand Up @@ -196,3 +147,82 @@ func (out *Renderer) writeTemplate(name string, data interface{}) (string, error

return result.String(), nil
}

func (out *Renderer) getTemplate(name string) *template.Template {
tmpl := template.New(name)

// Capture the base template funcs later because we need them with the right
// format that we got from the options.
baseTemplateFuncs := map[string]any{
"add": func(n1, n2 int) int {
return n1 + n2
},
"spacer": func() string {
return "\n\n"
},
"inlineSpacer": func() string {
return "\n"
},
"hangingIndent": func(s string, n int) string {
return strings.ReplaceAll(s, "\n", fmt.Sprintf("\n%s", strings.Repeat(" ", n)))
},
"include": func(name string, data any) (string, error) {
var b strings.Builder
err := tmpl.ExecuteTemplate(&b, name, data)
if err != nil {
return "", err
}

return b.String(), nil
},
"iter": func(l any) (any, error) {
type iter struct {
First bool
Last bool
Entry any
}

switch reflect.TypeOf(l).Kind() {
case reflect.Slice:
s := reflect.ValueOf(l)
out := make([]iter, s.Len())

for i := 0; i < s.Len(); i++ {
out[i] = iter{
First: i == 0,
Last: i == s.Len()-1,
Entry: s.Index(i).Interface(),
}
}

return out, nil
default:
return nil, fmt.Errorf("renderer: iter only accepts slices")
}
},

"bold": out.format.Bold,
"anchor": out.format.Anchor,
"anchorHeader": out.format.AnchorHeader,
"header": out.format.Header,
"rawAnchorHeader": out.format.RawAnchorHeader,
"rawHeader": out.format.RawHeader,
"codeBlock": out.format.CodeBlock,
"link": out.format.Link,
"listEntry": out.format.ListEntry,
"accordion": out.format.Accordion,
"accordionHeader": out.format.AccordionHeader,
"accordionTerminator": out.format.AccordionTerminator,
"localHref": out.format.LocalHref,
"rawLocalHref": out.format.RawLocalHref,
"codeHref": out.format.CodeHref,
"escape": out.format.Escape,
}

for n, f := range out.templateFuncs {
baseTemplateFuncs[n] = f
}

tmpl.Funcs(baseTemplateFuncs)
return tmpl
}
85 changes: 85 additions & 0 deletions renderer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package gomarkdoc_test

import (
"errors"
"go/build"
"os"
"strings"
"testing"

"github.com/matryer/is"
"github.com/princjef/gomarkdoc"
"github.com/princjef/gomarkdoc/format/formatcore"
"github.com/princjef/gomarkdoc/lang"
"github.com/princjef/gomarkdoc/logger"
)

func TestWithTemplateFunc(t *testing.T) {
is := is.New(t)

fn, err := loadFunc("./testData/docs", "Func")
is.NoErr(err)

r, err := gomarkdoc.NewRenderer()
is.NoErr(err)

r2, err := gomarkdoc.NewRenderer(
gomarkdoc.WithTemplateFunc("escape", func(text string) string {
return formatcore.Escape(strings.ToUpper(text))
}),
)
is.NoErr(err)

f, err := r.Func(fn)
is.NoErr(err)

f2, err := r2.Func(fn)
is.NoErr(err)

is.True(strings.Contains(f, "Func is present in this file."))
is.True(strings.Contains(f2, "FUNC IS PRESENT IN THIS FILE."))
}

func getBuildPackage(path string) (*build.Package, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}

return build.Import(path, wd, build.ImportComment)
}

func loadFunc(dir, name string) (*lang.Func, error) {
buildPkg, err := getBuildPackage(dir)
if err != nil {
return nil, err
}

log := logger.New(logger.ErrorLevel)
pkg, err := lang.NewPackageFromBuild(log, buildPkg)
if err != nil {
return nil, err
}

for _, f := range pkg.Funcs() {
if f.Name() == name {
return f, nil
}
}

for _, t := range pkg.Types() {
for _, f := range t.Funcs() {
if f.Name() == name {
return f, nil
}
}

for _, f := range t.Methods() {
if f.Name() == name {
return f, nil
}
}
}

return nil, errors.New("func not found")
}

0 comments on commit 0124a63

Please sign in to comment.