github.com/ajatprabha/gomarkdoc@v0.0.0-20230705102225-9d169ea523ff/renderer.go (about) 1 package gomarkdoc 2 3 import ( 4 "fmt" 5 "reflect" 6 "strings" 7 "text/template" 8 9 "github.com/ajatprabha/gomarkdoc/format" 10 "github.com/ajatprabha/gomarkdoc/lang" 11 ) 12 13 type ( 14 // Renderer provides capabilities for rendering various types of 15 // documentation with the configured format and templates. 16 Renderer struct { 17 templateOverrides map[string]string 18 tmpl *template.Template 19 format format.Format 20 templateFuncs map[string]any 21 } 22 23 // RendererOption configures the renderer's behavior. 24 RendererOption func(renderer *Renderer) error 25 ) 26 27 //go:generate ./gentmpl.sh templates templates 28 29 // NewRenderer initializes a Renderer configured using the provided options. If 30 // nothing special is provided, the created renderer will use the default set of 31 // templates and the GitHubFlavoredMarkdown. 32 func NewRenderer(opts ...RendererOption) (*Renderer, error) { 33 renderer := &Renderer{ 34 templateOverrides: make(map[string]string), 35 format: &format.GitHubFlavoredMarkdown{}, 36 templateFuncs: map[string]any{}, 37 } 38 39 for _, opt := range opts { 40 if err := opt(renderer); err != nil { 41 return nil, err 42 } 43 } 44 45 for name, tmplStr := range templates { 46 // Use the override if present 47 if val, ok := renderer.templateOverrides[name]; ok { 48 tmplStr = val 49 } 50 51 if renderer.tmpl == nil { 52 tmpl := renderer.getTemplate(name) 53 54 if _, err := tmpl.Parse(tmplStr); err != nil { 55 return nil, err 56 } 57 58 renderer.tmpl = tmpl 59 } else if _, err := renderer.tmpl.New(name).Parse(tmplStr); err != nil { 60 return nil, err 61 } 62 } 63 64 return renderer, nil 65 } 66 67 // WithTemplateOverride adds a template that overrides the template with the 68 // provided name using the value provided in the tmpl parameter. 69 func WithTemplateOverride(name, tmpl string) RendererOption { 70 return func(renderer *Renderer) error { 71 if _, ok := templates[name]; !ok { 72 return fmt.Errorf(`gomarkdoc: invalid template name "%s"`, name) 73 } 74 75 renderer.templateOverrides[name] = tmpl 76 77 return nil 78 } 79 } 80 81 // WithFormat changes the renderer to use the format provided instead of the 82 // default format. 83 func WithFormat(format format.Format) RendererOption { 84 return func(renderer *Renderer) error { 85 renderer.format = format 86 return nil 87 } 88 } 89 90 // WithTemplateFunc adds the provided function with the given name to the list 91 // of functions that can be used by the rendering templates. 92 // 93 // Any name collisions between built-in functions and functions provided here 94 // are resolved in favor of the function provided here, so be careful about the 95 // naming of your functions to avoid overriding existing behavior unless 96 // desired. 97 func WithTemplateFunc(name string, fn any) RendererOption { 98 return func(renderer *Renderer) error { 99 renderer.templateFuncs[name] = fn 100 return nil 101 } 102 } 103 104 // File renders a file containing one or more packages to document to a string. 105 // You can change the rendering of the file by overriding the "file" template 106 // or one of the templates it references. 107 func (out *Renderer) File(file *lang.File) (string, error) { 108 return out.writeTemplate("file", file) 109 } 110 111 // Package renders a package's documentation to a string. You can change the 112 // rendering of the package by overriding the "package" template or one of the 113 // templates it references. 114 func (out *Renderer) Package(pkg *lang.Package) (string, error) { 115 return out.writeTemplate("package", pkg) 116 } 117 118 // Func renders a function's documentation to a string. You can change the 119 // rendering of the package by overriding the "func" template or one of the 120 // templates it references. 121 func (out *Renderer) Func(fn *lang.Func) (string, error) { 122 return out.writeTemplate("func", fn) 123 } 124 125 // Type renders a type's documentation to a string. You can change the 126 // rendering of the type by overriding the "type" template or one of the 127 // templates it references. 128 func (out *Renderer) Type(typ *lang.Type) (string, error) { 129 return out.writeTemplate("type", typ) 130 } 131 132 // Example renders an example's documentation to a string. You can change the 133 // rendering of the example by overriding the "example" template or one of the 134 // templates it references. 135 func (out *Renderer) Example(ex *lang.Example) (string, error) { 136 return out.writeTemplate("example", ex) 137 } 138 139 // writeTemplate renders the template of the provided name using the provided 140 // data object to a string. It uses the set of templates provided to the 141 // renderer as a template library. 142 func (out *Renderer) writeTemplate(name string, data interface{}) (string, error) { 143 var result strings.Builder 144 if err := out.tmpl.ExecuteTemplate(&result, name, data); err != nil { 145 return "", err 146 } 147 148 return result.String(), nil 149 } 150 151 func (out *Renderer) getTemplate(name string) *template.Template { 152 tmpl := template.New(name) 153 154 // Capture the base template funcs later because we need them with the right 155 // format that we got from the options. 156 baseTemplateFuncs := map[string]any{ 157 "add": func(n1, n2 int) int { 158 return n1 + n2 159 }, 160 "spacer": func() string { 161 return "\n\n" 162 }, 163 "inlineSpacer": func() string { 164 return "\n" 165 }, 166 "hangingIndent": func(s string, n int) string { 167 return strings.ReplaceAll(s, "\n", fmt.Sprintf("\n%s", strings.Repeat(" ", n))) 168 }, 169 "include": func(name string, data any) (string, error) { 170 var b strings.Builder 171 err := tmpl.ExecuteTemplate(&b, name, data) 172 if err != nil { 173 return "", err 174 } 175 176 return b.String(), nil 177 }, 178 "iter": func(l any) (any, error) { 179 type iter struct { 180 First bool 181 Last bool 182 Entry any 183 } 184 185 switch reflect.TypeOf(l).Kind() { 186 case reflect.Slice: 187 s := reflect.ValueOf(l) 188 out := make([]iter, s.Len()) 189 190 for i := 0; i < s.Len(); i++ { 191 out[i] = iter{ 192 First: i == 0, 193 Last: i == s.Len()-1, 194 Entry: s.Index(i).Interface(), 195 } 196 } 197 198 return out, nil 199 default: 200 return nil, fmt.Errorf("renderer: iter only accepts slices") 201 } 202 }, 203 204 "bold": out.format.Bold, 205 "anchor": out.format.Anchor, 206 "anchorHeader": out.format.AnchorHeader, 207 "header": out.format.Header, 208 "rawAnchorHeader": out.format.RawAnchorHeader, 209 "rawHeader": out.format.RawHeader, 210 "codeBlock": out.format.CodeBlock, 211 "link": out.format.Link, 212 "listEntry": out.format.ListEntry, 213 "accordion": out.format.Accordion, 214 "accordionHeader": out.format.AccordionHeader, 215 "accordionTerminator": out.format.AccordionTerminator, 216 "localHref": out.format.LocalHref, 217 "rawLocalHref": out.format.RawLocalHref, 218 "codeHref": out.format.CodeHref, 219 "escape": out.format.Escape, 220 } 221 222 for n, f := range out.templateFuncs { 223 baseTemplateFuncs[n] = f 224 } 225 226 tmpl.Funcs(baseTemplateFuncs) 227 return tmpl 228 }