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  }