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")
+}