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  }