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