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  }