saasitone/pkg/services/template_renderer.go

221 lines
6.3 KiB
Go
Raw Normal View History

2021-12-19 17:09:01 -08:00
package services
import (
"bytes"
"errors"
"fmt"
"html/template"
"io/fs"
2021-12-19 17:09:01 -08:00
"sync"
"github.com/labstack/echo/v4"
2022-01-01 07:44:18 -08:00
"github.com/mikestefanello/pagoda/config"
2022-11-02 16:23:26 -07:00
"github.com/mikestefanello/pagoda/pkg/funcmap"
"github.com/mikestefanello/pagoda/templates"
2021-12-19 17:09:01 -08:00
)
type (
// TemplateRenderer provides a flexible and easy to use method of rendering simple templates or complex sets of
// templates while also providing caching and/or hot-reloading depending on your current environment
TemplateRenderer struct {
// templateCache stores a cache of parsed page templates
templateCache sync.Map
2021-12-19 17:09:01 -08:00
// funcMap stores the template function map
funcMap template.FuncMap
2021-12-19 17:09:01 -08:00
// config stores application configuration
config *config.Config
}
// TemplateParsed is a wrapper around parsed templates which are stored in the TemplateRenderer cache
TemplateParsed struct {
// Template is the parsed template
Template *template.Template
// build stores the build data used to parse the template
build *templateBuild
}
// templateBuild stores the build data used to parse a template
templateBuild struct {
group string
key string
base string
files []string
directories []string
}
// templateBuilder handles chaining a template parse operation
templateBuilder struct {
build *templateBuild
renderer *TemplateRenderer
}
)
2021-12-19 17:09:01 -08:00
2021-12-22 16:18:33 -08:00
// NewTemplateRenderer creates a new TemplateRenderer
func NewTemplateRenderer(cfg *config.Config, web *echo.Echo) *TemplateRenderer {
return &TemplateRenderer{
2021-12-19 17:09:01 -08:00
templateCache: sync.Map{},
funcMap: funcmap.NewFuncMap(web),
2021-12-19 17:09:01 -08:00
config: cfg,
}
}
// Parse creates a template build operation
func (t *TemplateRenderer) Parse() *templateBuilder {
return &templateBuilder{
renderer: t,
build: &templateBuild{},
}
}
// getCacheKey gets a cache key for a given group and ID
func (t *TemplateRenderer) getCacheKey(group, key string) string {
if group != "" {
return fmt.Sprintf("%s:%s", group, key)
}
return key
}
// parse parses a set of templates and caches them for quick execution
// If the application environment is set to local, the cache will be bypassed and templates will be
// parsed upon each request so hot-reloading is possible without restarts.
// Also included will be the function map provided by the funcmap package.
func (t *TemplateRenderer) parse(build *templateBuild) (*TemplateParsed, error) {
var tp *TemplateParsed
var err error
switch {
case build.key == "":
return nil, errors.New("cannot parse template without key")
case len(build.files) == 0 && len(build.directories) == 0:
return nil, errors.New("cannot parse template without files or directories")
case build.base == "":
return nil, errors.New("cannot parse template without base")
}
// Generate the cache key
cacheKey := t.getCacheKey(build.group, build.key)
2021-12-19 17:09:01 -08:00
// Check if the template has not yet been parsed or if the app environment is local, so that
// templates reflect changes without having the restart the server
if tp, err = t.Load(build.group, build.key); err != nil || t.config.App.Environment == config.EnvLocal {
2021-12-19 17:09:01 -08:00
// Initialize the parsed template with the function map
parsed := template.New(build.base + config.TemplateExt).
2021-12-19 17:09:01 -08:00
Funcs(t.funcMap)
// Format the requested files
for k, v := range build.files {
build.files[k] = fmt.Sprintf("%s%s", v, config.TemplateExt)
}
// Include all files within the requested directories
for k, v := range build.directories {
build.directories[k] = fmt.Sprintf("%s/*%s", v, config.TemplateExt)
}
2021-12-19 17:09:01 -08:00
// Get the templates
var tpl fs.FS
if t.config.App.Environment == config.EnvLocal {
tpl = templates.GetOS()
} else {
tpl = templates.Get()
2021-12-19 17:09:01 -08:00
}
// Parse the templates
parsed, err = parsed.ParseFS(tpl, append(build.files, build.directories...)...)
if err != nil {
return nil, err
2021-12-19 17:09:01 -08:00
}
// Store the template so this process only happens once
tp = &TemplateParsed{
Template: parsed,
build: build,
}
t.templateCache.Store(cacheKey, tp)
2021-12-19 17:09:01 -08:00
}
return tp, nil
2021-12-19 17:09:01 -08:00
}
// Load loads a template from the cache
func (t *TemplateRenderer) Load(group, key string) (*TemplateParsed, error) {
load, ok := t.templateCache.Load(t.getCacheKey(group, key))
if !ok {
return nil, errors.New("uncached page template requested")
2021-12-19 17:09:01 -08:00
}
tmpl, ok := load.(*TemplateParsed)
if !ok {
return nil, errors.New("unable to cast cached template")
2021-12-19 17:09:01 -08:00
}
return tmpl, nil
2021-12-19 17:09:01 -08:00
}
// Execute executes a template with the given data and provides the output
func (t *TemplateParsed) Execute(data any) (*bytes.Buffer, error) {
if t.Template == nil {
return nil, errors.New("cannot execute template: template not initialized")
}
buf := new(bytes.Buffer)
err := t.Template.ExecuteTemplate(buf, t.build.base+config.TemplateExt, data)
if err != nil {
return nil, err
}
return buf, nil
}
// Group sets the cache group for the template being built
func (t *templateBuilder) Group(group string) *templateBuilder {
t.build.group = group
return t
}
2021-12-19 17:09:01 -08:00
// Key sets the cache key for the template being built
func (t *templateBuilder) Key(key string) *templateBuilder {
t.build.key = key
return t
}
2021-12-19 17:09:01 -08:00
// Base sets the name of the base template to be used during template parsing and execution.
// This should be only the file name without a directory or extension.
func (t *templateBuilder) Base(base string) *templateBuilder {
t.build.base = base
return t
2021-12-19 17:09:01 -08:00
}
// Files sets a list of template files to include in the parse.
// This should not include the file extension and the paths should be relative to the templates directory.
func (t *templateBuilder) Files(files ...string) *templateBuilder {
t.build.files = files
return t
2021-12-19 17:09:01 -08:00
}
// Directories sets a list of directories that all template files within will be parsed.
// The paths should be relative to the templates directory.
func (t *templateBuilder) Directories(directories ...string) *templateBuilder {
t.build.directories = directories
return t
}
// Store parsed the templates and stores them in the cache
func (t *templateBuilder) Store() (*TemplateParsed, error) {
return t.renderer.parse(t.build)
}
// Execute executes the template with the given data.
// If the template has not already been cached, this will parse and cache the template
func (t *templateBuilder) Execute(data any) (*bytes.Buffer, error) {
tp, err := t.Store()
if err != nil {
return nil, err
}
return tp.Execute(data)
2021-12-19 17:09:01 -08:00
}