diff --git a/README.md b/README.md index ed01465..e0a5bf0 100644 --- a/README.md +++ b/README.md @@ -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>) -## type [Renderer]() +## type [Renderer]() Renderer provides capabilities for rendering various types of documentation with the configured format and templates. @@ -253,7 +254,7 @@ type Renderer struct { ``` -### func [NewRenderer]() +### func [NewRenderer]() ```go func NewRenderer(opts ...RendererOption) (*Renderer, error) @@ -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. -### func \(\*Renderer\) [Example]() +### func \(\*Renderer\) [Example]() ```go func (out *Renderer) Example(ex *lang.Example) (string, error) @@ -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. -### func \(\*Renderer\) [File]() +### func \(\*Renderer\) [File]() ```go func (out *Renderer) File(file *lang.File) (string, error) @@ -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. -### func \(\*Renderer\) [Func]() +### func \(\*Renderer\) [Func]() ```go func (out *Renderer) Func(fn *lang.Func) (string, error) @@ -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. -### func \(\*Renderer\) [Package]() +### func \(\*Renderer\) [Package]() ```go func (out *Renderer) Package(pkg *lang.Package) (string, error) @@ -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. -### func \(\*Renderer\) [Type]() +### func \(\*Renderer\) [Type]() ```go func (out *Renderer) Type(typ *lang.Type) (string, error) @@ -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. -## type [RendererOption]() +## type [RendererOption]() RendererOption configures the renderer's behavior. @@ -316,7 +317,7 @@ type RendererOption func(renderer *Renderer) error ``` -### func [WithFormat]() +### func [WithFormat]() ```go func WithFormat(format format.Format) RendererOption @@ -324,8 +325,19 @@ func WithFormat(format format.Format) RendererOption WithFormat changes the renderer to use the format provided instead of the default format. + +### func [WithTemplateFunc]() + +```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. + -### func [WithTemplateOverride]() +### func [WithTemplateOverride]() ```go func WithTemplateOverride(name, tmpl string) RendererOption diff --git a/renderer.go b/renderer.go index 4f82bf7..6ad44fb 100644 --- a/renderer.go +++ b/renderer.go @@ -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. @@ -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 { @@ -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 @@ -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. @@ -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 +} diff --git a/renderer_test.go b/renderer_test.go new file mode 100644 index 0000000..83e69dd --- /dev/null +++ b/renderer_test.go @@ -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") +}