added helper html template rendering utils
This commit is contained in:
parent
13c0572fe1
commit
4f3ca6fe2b
|
@ -0,0 +1,76 @@
|
||||||
|
// Package template is a thin wrapper arround the standard html/template
|
||||||
|
// and text/template packages that implements a convenient registry to
|
||||||
|
// load and cache templates on the fly concurrently.
|
||||||
|
//
|
||||||
|
// It was created to assist the JSVM plugin HTML rendering, but could be used in other Go code.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// registry := template.NewRegistry()
|
||||||
|
//
|
||||||
|
// html1, err := registry.LoadFiles(
|
||||||
|
// // the files set wil be parsed only once and then cached
|
||||||
|
// "layout.html",
|
||||||
|
// "content.html",
|
||||||
|
// ).Render(map[string]any{"name": "John"})
|
||||||
|
//
|
||||||
|
// html2, err := registry.LoadFiles(
|
||||||
|
// // reuse the already parsed and cached files set
|
||||||
|
// "layout.html",
|
||||||
|
// "content.html",
|
||||||
|
// ).Render(map[string]any{"name": "Jane"})
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tools/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRegistry creates and initializes a new blank templates registry.
|
||||||
|
//
|
||||||
|
// Use the Registry.Load* methods to load templates into the registry.
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
cache: store.New[*Renderer](nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry defines a templates registry that is safe to be used by multiple goroutines.
|
||||||
|
//
|
||||||
|
// Use the Registry.Load* methods to load templates into the registry.
|
||||||
|
type Registry struct {
|
||||||
|
cache *store.Store[*Renderer]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFiles caches (if not already) the specified files set as a
|
||||||
|
// single template and returns a ready to use Renderer instance.
|
||||||
|
func (r *Registry) LoadFiles(files ...string) *Renderer {
|
||||||
|
key := strings.Join(files, ",")
|
||||||
|
|
||||||
|
found := r.cache.Get(key)
|
||||||
|
|
||||||
|
if found == nil {
|
||||||
|
// parse and cache
|
||||||
|
tpl, err := template.ParseFiles(files...)
|
||||||
|
found = &Renderer{template: tpl, parseError: err}
|
||||||
|
r.cache.Set(key, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadString caches (if not already) the specified inline string as a
|
||||||
|
// single template and returns a ready to use Renderer instance.
|
||||||
|
func (r *Registry) LoadString(text string) *Renderer {
|
||||||
|
found := r.cache.Get(text)
|
||||||
|
|
||||||
|
if found == nil {
|
||||||
|
// parse and cache (using the text as key)
|
||||||
|
tpl, err := template.New("").Parse(text)
|
||||||
|
found = &Renderer{template: tpl, parseError: err}
|
||||||
|
r.cache.Set(text, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRegistry(t *testing.T) {
|
||||||
|
r := NewRegistry()
|
||||||
|
|
||||||
|
if r.cache == nil {
|
||||||
|
t.Fatalf("Expected cache store to be initialized, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := r.cache.Length(); v != 0 {
|
||||||
|
t.Fatalf("Expected cache store length to be 0, got %d", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryLoadFiles(t *testing.T) {
|
||||||
|
r := NewRegistry()
|
||||||
|
|
||||||
|
t.Run("invalid or missing files", func(t *testing.T) {
|
||||||
|
r.LoadFiles("file1.missing", "file2.missing")
|
||||||
|
|
||||||
|
key := "file1.missing,file2.missing"
|
||||||
|
renderer := r.cache.Get(key)
|
||||||
|
|
||||||
|
if renderer == nil {
|
||||||
|
t.Fatal("Expected renderer to be initialized even if invalid, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderer.template != nil {
|
||||||
|
t.Fatalf("Expected renderer template to be nil, got %v", renderer.template)
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderer.parseError == nil {
|
||||||
|
t.Fatalf("Expected renderer parseError to be set, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid files", func(t *testing.T) {
|
||||||
|
// create test templates
|
||||||
|
dir, err := os.MkdirTemp(os.TempDir(), "template_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "base.html"), []byte(`Base:{{template "content"}}`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "content.html"), []byte(`{{define "content"}}Content:123{{end}}`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
files := []string{filepath.Join(dir, "base.html"), filepath.Join(dir, "content.html")}
|
||||||
|
|
||||||
|
r.LoadFiles(files...)
|
||||||
|
|
||||||
|
renderer := r.cache.Get(strings.Join(files, ","))
|
||||||
|
|
||||||
|
if renderer == nil {
|
||||||
|
t.Fatal("Expected renderer to be initialized even if invalid, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderer.template == nil {
|
||||||
|
t.Fatal("Expected renderer template to be set, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderer.parseError != nil {
|
||||||
|
t.Fatalf("Expected renderer parseError to be nil, got %v", renderer.parseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := renderer.Render(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected Render() error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Base:Content:123"
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected Render() result %q, got %q", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Renderer defines a single parsed template.
|
||||||
|
type Renderer struct {
|
||||||
|
template *template.Template
|
||||||
|
parseError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render executes the template with the specified data as the dot object
|
||||||
|
// and returns the result as plain string.
|
||||||
|
func (r *Renderer) Render(data any) (string, error) {
|
||||||
|
if r.parseError != nil {
|
||||||
|
return "", r.parseError
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.template == nil {
|
||||||
|
return "", errors.New("invalid or nil template")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := r.template.Execute(buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRendererRender(t *testing.T) {
|
||||||
|
tpl, _ := template.New("").Parse("Hello {{.Name}}!")
|
||||||
|
tpl.Option("missingkey=error") // enforce execute errors
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
renderer *Renderer
|
||||||
|
data any
|
||||||
|
expectedHasErr bool
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
"with nil template": {
|
||||||
|
&Renderer{},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
"with parse error": {
|
||||||
|
&Renderer{
|
||||||
|
template: tpl,
|
||||||
|
parseError: errors.New("test"),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
"with execute error": {
|
||||||
|
&Renderer{template: tpl},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
"no error": {
|
||||||
|
&Renderer{template: tpl},
|
||||||
|
struct{ Name string }{"world"},
|
||||||
|
false,
|
||||||
|
"Hello world!",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, s := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
result, err := s.renderer.Render(s.data)
|
||||||
|
|
||||||
|
hasErr := err != nil
|
||||||
|
|
||||||
|
if s.expectedHasErr != hasErr {
|
||||||
|
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectedHasErr, hasErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.expectedResult != result {
|
||||||
|
t.Fatalf("Expected result %v, got %v", s.expectedResult, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue