template/html: refactor template loading
Allow customization of the template loading process by providing a Loader implementation. Allow auto-reloading of templates in development mode.
This commit is contained in:
parent
f48503f2b6
commit
985ce3eba3
|
@ -0,0 +1,56 @@
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Loader interface {
|
||||||
|
Load(*TemplateService) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DirectoryLoader struct {
|
||||||
|
rootDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DirectoryLoader) Load(srv *TemplateService) error {
|
||||||
|
blockFiles, err := filepath.Glob(filepath.Join(l.rootDir, "blocks", "*.tmpl"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range blockFiles {
|
||||||
|
blockContent, err := ioutil.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
blockName := filepath.Base(f)
|
||||||
|
srv.AddBlock(blockName, string(blockContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutFiles, err := filepath.Glob(filepath.Join(l.rootDir, "layouts", "*.tmpl"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate our templates map from our layouts/ and blocks/ directories
|
||||||
|
for _, f := range layoutFiles {
|
||||||
|
templateData, err := ioutil.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
templateName := filepath.Base(f)
|
||||||
|
|
||||||
|
if err := srv.LoadLayout(templateName, string(templateData)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirectoryLoader(rootDir string) *DirectoryLoader {
|
||||||
|
return &DirectoryLoader{rootDir}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import "html/template"
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Helpers template.FuncMap
|
Helpers template.FuncMap
|
||||||
PoolSize int
|
PoolSize int
|
||||||
|
DevMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionFunc configures options for the template service
|
// OptionFunc configures options for the template service
|
||||||
|
@ -48,6 +49,14 @@ func WithPoolSize(size int) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDevMode configures the template service
|
||||||
|
// to use the development mode (auto reload of templates).
|
||||||
|
func WithDevMode(enabled bool) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.DevMode = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
func defaultOptions() *Options {
|
||||||
options := &Options{}
|
options := &Options{}
|
||||||
funcs := []OptionFunc{
|
funcs := []OptionFunc{
|
||||||
|
|
|
@ -4,9 +4,9 @@ import "gitlab.com/wpetit/goweb/service"
|
||||||
|
|
||||||
// ServiceProvider returns a service.Provider for the
|
// ServiceProvider returns a service.Provider for the
|
||||||
// the HTML template service implementation
|
// the HTML template service implementation
|
||||||
func ServiceProvider(templateDir string, funcs ...OptionFunc) service.Provider {
|
func ServiceProvider(loader Loader, funcs ...OptionFunc) service.Provider {
|
||||||
templateService := NewTemplateService(funcs...)
|
templateService := NewTemplateService(loader, funcs...)
|
||||||
err := templateService.LoadTemplatesDir(templateDir)
|
err := templateService.Load()
|
||||||
return func(container *service.Container) (interface{}, error) {
|
return func(container *service.Container) (interface{}, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,91 +1,79 @@
|
||||||
package html
|
package html
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"sync"
|
||||||
|
|
||||||
"github.com/oxtoacart/bpool"
|
"github.com/oxtoacart/bpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrLayoutNotFound = errors.New("layout not found")
|
||||||
|
)
|
||||||
|
|
||||||
// TemplateService is a template/html based templating service
|
// TemplateService is a template/html based templating service
|
||||||
type TemplateService struct {
|
type TemplateService struct {
|
||||||
templates map[string]*template.Template
|
mutex sync.RWMutex
|
||||||
pool *bpool.BufferPool
|
blocks map[string]string
|
||||||
helpers template.FuncMap
|
layouts map[string]*template.Template
|
||||||
|
loader Loader
|
||||||
|
pool *bpool.BufferPool
|
||||||
|
helpers template.FuncMap
|
||||||
|
devMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TemplateService) LoadLayout(name string, layout string, blocks []string) error {
|
func (t *TemplateService) AddBlock(name string, blockContent string) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
t.blocks[name] = blockContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TemplateService) LoadLayout(name string, layoutContent string) error {
|
||||||
tmpl := template.New(name)
|
tmpl := template.New(name)
|
||||||
tmpl.Funcs(t.helpers)
|
tmpl.Funcs(t.helpers)
|
||||||
|
|
||||||
for _, b := range blocks {
|
t.mutex.RLock()
|
||||||
|
for _, b := range t.blocks {
|
||||||
if _, err := tmpl.Parse(b); err != nil {
|
if _, err := tmpl.Parse(b); err != nil {
|
||||||
|
t.mutex.RUnlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.mutex.RUnlock()
|
||||||
|
|
||||||
tmpl, err := tmpl.Parse(layout)
|
tmpl, err := tmpl.Parse(layoutContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.templates[name] = tmpl
|
t.mutex.Lock()
|
||||||
|
t.layouts[name] = tmpl
|
||||||
|
t.mutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadTemplatesDir loads the templates used by the service
|
// Load parses templates via the configured template loader.
|
||||||
func (t *TemplateService) LoadTemplatesDir(templatesDir string) error {
|
func (t *TemplateService) Load() error {
|
||||||
layoutFiles, err := filepath.Glob(filepath.Join(templatesDir, "layouts", "*.tmpl"))
|
return t.loader.Load(t)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockFiles, err := filepath.Glob(filepath.Join(templatesDir, "blocks", "*.tmpl"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockTemplates := make([]string, 0, len(blockFiles))
|
|
||||||
|
|
||||||
for _, f := range blockFiles {
|
|
||||||
templateData, err := ioutil.ReadFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockTemplates = append(blockTemplates, string(templateData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate our templates map from our layouts/ and blocks/ directories
|
|
||||||
for _, f := range layoutFiles {
|
|
||||||
templateData, err := ioutil.ReadFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
templateName := filepath.Base(f)
|
|
||||||
|
|
||||||
if err := t.LoadLayout(templateName, string(templateData), blockTemplates); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPage renders a template to the given http.ResponseWriter
|
// RenderPage renders a template to the given http.ResponseWriter
|
||||||
func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error {
|
func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error {
|
||||||
|
if t.devMode {
|
||||||
|
if err := t.loader.Load(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the template exists in the map.
|
// Ensure the template exists in the map.
|
||||||
tmpl, ok := t.templates[templateName]
|
tmpl, ok := t.layouts[templateName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("the template '%s' does not exist", templateName)
|
return ErrLayoutNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a buffer to temporarily write to and check if any errors were encountered.
|
// Create a buffer to temporarily write to and check if any errors were encountered.
|
||||||
|
@ -103,20 +91,25 @@ func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a template to the given io.Writer
|
// Render renders a layout to the given io.Writer.
|
||||||
func (t *TemplateService) Render(w io.Writer, templateName string, data interface{}) error {
|
func (t *TemplateService) Render(w io.Writer, layoutName string, data interface{}) error {
|
||||||
|
if t.devMode {
|
||||||
|
if err := t.loader.Load(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the template exists in the map.
|
// Ensure the template exists in the map.
|
||||||
tmpl, ok := t.templates[templateName]
|
tmpl, ok := t.layouts[layoutName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("the template '%s' does not exist", templateName)
|
return ErrLayoutNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a buffer to temporarily write to and check if any errors were encountered.
|
// Create a buffer to temporarily write to and check if any errors were encountered.
|
||||||
buf := t.pool.Get()
|
buf := t.pool.Get()
|
||||||
defer t.pool.Put(buf)
|
defer t.pool.Put(buf)
|
||||||
|
|
||||||
if err := tmpl.ExecuteTemplate(buf, templateName, data); err != nil {
|
if err := tmpl.ExecuteTemplate(buf, layoutName, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,14 +119,17 @@ func (t *TemplateService) Render(w io.Writer, templateName string, data interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTemplateService returns a new Service
|
// NewTemplateService returns a new Service
|
||||||
func NewTemplateService(funcs ...OptionFunc) *TemplateService {
|
func NewTemplateService(loader Loader, funcs ...OptionFunc) *TemplateService {
|
||||||
options := defaultOptions()
|
options := defaultOptions()
|
||||||
for _, f := range funcs {
|
for _, f := range funcs {
|
||||||
f(options)
|
f(options)
|
||||||
}
|
}
|
||||||
return &TemplateService{
|
return &TemplateService{
|
||||||
templates: make(map[string]*template.Template),
|
blocks: make(map[string]string),
|
||||||
pool: bpool.NewBufferPool(options.PoolSize),
|
layouts: make(map[string]*template.Template),
|
||||||
helpers: options.Helpers,
|
pool: bpool.NewBufferPool(options.PoolSize),
|
||||||
|
helpers: options.Helpers,
|
||||||
|
loader: loader,
|
||||||
|
devMode: options.DevMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue