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