Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
141
tools/template/registry.go
Normal file
141
tools/template/registry.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Package template is a thin wrapper around 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 (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
)
|
||||
|
||||
// NewRegistry creates and initializes a new templates registry with
|
||||
// some defaults (eg. global "raw" template function for unescaped HTML).
|
||||
//
|
||||
// Use the Registry.Load* methods to load templates into the registry.
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
cache: store.New[string, *Renderer](nil),
|
||||
funcs: template.FuncMap{
|
||||
"raw": func(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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[string, *Renderer]
|
||||
funcs template.FuncMap
|
||||
}
|
||||
|
||||
// AddFuncs registers new global template functions.
|
||||
//
|
||||
// The key of each map entry is the function name that will be used in the templates.
|
||||
// If a function with the map entry name already exists it will be replaced with the new one.
|
||||
//
|
||||
// The value of each map entry is a function that must have either a
|
||||
// single return value, or two return values of which the second has type error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// r.AddFuncs(map[string]any{
|
||||
// "toUpper": func(str string) string {
|
||||
// return strings.ToUppser(str)
|
||||
// },
|
||||
// ...
|
||||
// })
|
||||
func (r *Registry) AddFuncs(funcs map[string]any) *Registry {
|
||||
for name, f := range funcs {
|
||||
r.funcs[name] = f
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// LoadFiles caches (if not already) the specified filenames set as a
|
||||
// single template and returns a ready to use Renderer instance.
|
||||
//
|
||||
// There must be at least 1 filename specified.
|
||||
func (r *Registry) LoadFiles(filenames ...string) *Renderer {
|
||||
key := strings.Join(filenames, ",")
|
||||
|
||||
found := r.cache.Get(key)
|
||||
|
||||
if found == nil {
|
||||
// parse and cache
|
||||
tpl, err := template.New(filepath.Base(filenames[0])).Funcs(r.funcs).ParseFiles(filenames...)
|
||||
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("").Funcs(r.funcs).Parse(text)
|
||||
found = &Renderer{template: tpl, parseError: err}
|
||||
r.cache.Set(text, found)
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// LoadFS caches (if not already) the specified fs and globPatterns
|
||||
// pair as single template and returns a ready to use Renderer instance.
|
||||
//
|
||||
// There must be at least 1 file matching the provided globPattern(s)
|
||||
// (note that most file names serves as glob patterns matching themselves).
|
||||
func (r *Registry) LoadFS(fsys fs.FS, globPatterns ...string) *Renderer {
|
||||
key := fmt.Sprintf("%v%v", fsys, globPatterns)
|
||||
|
||||
found := r.cache.Get(key)
|
||||
|
||||
if found == nil {
|
||||
// find the first file to use as template name (it is required when specifying Funcs)
|
||||
var firstFilename string
|
||||
if len(globPatterns) > 0 {
|
||||
list, _ := fs.Glob(fsys, globPatterns[0])
|
||||
if len(list) > 0 {
|
||||
firstFilename = filepath.Base(list[0])
|
||||
}
|
||||
}
|
||||
|
||||
tpl, err := template.New(firstFilename).Funcs(r.funcs).ParseFS(fsys, globPatterns...)
|
||||
found = &Renderer{template: tpl, parseError: err}
|
||||
r.cache.Set(key, found)
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
250
tools/template/registry_test.go
Normal file
250
tools/template/registry_test.go
Normal file
|
@ -0,0 +1,250 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func checkRegistryFuncs(t *testing.T, r *Registry, expectedFuncs ...string) {
|
||||
if v := len(r.funcs); v != len(expectedFuncs) {
|
||||
t.Fatalf("Expected total %d funcs, got %d", len(expectedFuncs), v)
|
||||
}
|
||||
|
||||
for _, name := range expectedFuncs {
|
||||
if _, ok := r.funcs[name]; !ok {
|
||||
t.Fatalf("Missing %q func", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
checkRegistryFuncs(t, r, "raw")
|
||||
}
|
||||
|
||||
func TestRegistryAddFuncs(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
r.AddFuncs(map[string]any{
|
||||
"test": func(a string) string { return a + "-TEST" },
|
||||
})
|
||||
|
||||
checkRegistryFuncs(t, r, "raw", "test")
|
||||
|
||||
result, err := r.LoadString(`{{.|test}}`).Render("example")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Render() error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "example-TEST"
|
||||
if result != expected {
|
||||
t.Fatalf("Expected Render() result %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
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:{{.|raw}}{{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("<h1>123</h1>")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Render() error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "Base:Content:<h1>123</h1>"
|
||||
if result != expected {
|
||||
t.Fatalf("Expected Render() result %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistryLoadString(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
t.Run("invalid template string", func(t *testing.T) {
|
||||
txt := `test {{define "content"}}`
|
||||
|
||||
r.LoadString(txt)
|
||||
|
||||
renderer := r.cache.Get(txt)
|
||||
|
||||
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 template string", func(t *testing.T) {
|
||||
txt := `test {{.|raw}}`
|
||||
|
||||
r.LoadString(txt)
|
||||
|
||||
renderer := r.cache.Get(txt)
|
||||
|
||||
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("<h1>123</h1>")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Render() error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "test <h1>123</h1>"
|
||||
if result != expected {
|
||||
t.Fatalf("Expected Render() result %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistryLoadFS(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
t.Run("invalid fs", func(t *testing.T) {
|
||||
fs := os.DirFS("__missing__")
|
||||
|
||||
files := []string{"missing1", "missing2"}
|
||||
|
||||
key := fmt.Sprintf("%v%v", fs, files)
|
||||
|
||||
r.LoadFS(fs, files...)
|
||||
|
||||
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 fs", func(t *testing.T) {
|
||||
// create test templates
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "template_test2")
|
||||
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:{{.|raw}}{{end}}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
fs := os.DirFS(dir)
|
||||
|
||||
files := []string{"base.html", "content.html"}
|
||||
|
||||
key := fmt.Sprintf("%v%v", fs, files)
|
||||
|
||||
r.LoadFS(fs, files...)
|
||||
|
||||
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.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("<h1>123</h1>")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Render() error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "Base:Content:<h1>123</h1>"
|
||||
if result != expected {
|
||||
t.Fatalf("Expected Render() result %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
33
tools/template/renderer.go
Normal file
33
tools/template/renderer.go
Normal file
|
@ -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
|
||||
}
|
63
tools/template/renderer_test.go
Normal file
63
tools/template/renderer_test.go
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue