Add ability to load templates live from the filesystem
See config.go.example.
This commit is contained in:
parent
3a66b7a77d
commit
524cf8e27b
|
@ -82,4 +82,7 @@ var Config = HMNConfig{
|
|||
CineraOutputPath: "./annotations/",
|
||||
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
|
||||
},
|
||||
DevConfig: DevConfig{
|
||||
LiveTemplates: true,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ type HMNConfig struct {
|
|||
Discord DiscordConfig
|
||||
Twitch TwitchConfig
|
||||
EpisodeGuide EpisodeGuide
|
||||
DevConfig DevConfig
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
|
@ -101,6 +102,10 @@ type AdminConfig struct {
|
|||
AtomPassword string
|
||||
}
|
||||
|
||||
type DevConfig struct {
|
||||
LiveTemplates bool // load templates live from the filesystem instead of embedding them
|
||||
}
|
||||
|
||||
func init() {
|
||||
if Config.EpisodeGuide.Projects == nil {
|
||||
Config.EpisodeGuide.Projects = make(map[string]string)
|
||||
|
|
|
@ -99,10 +99,7 @@ func IsEmail(address string) bool {
|
|||
|
||||
func renderTemplate(name string, data interface{}) (string, error) {
|
||||
var buffer bytes.Buffer
|
||||
template, hasTemplate := templates.Templates[name]
|
||||
if !hasTemplate {
|
||||
return "", oops.New(nil, "Template not found: %s", name)
|
||||
}
|
||||
template := templates.GetTemplate(name)
|
||||
err := template.Execute(&buffer, data)
|
||||
if err != nil {
|
||||
return "", oops.New(err, "Failed to render template for email")
|
||||
|
|
|
@ -4,13 +4,16 @@ import (
|
|||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/auth"
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/google/uuid"
|
||||
|
@ -25,22 +28,22 @@ const (
|
|||
)
|
||||
|
||||
//go:embed src
|
||||
var templateFs embed.FS
|
||||
var Templates map[string]*template.Template
|
||||
var embeddedTemplateFs embed.FS
|
||||
var embeddedTemplates map[string]*template.Template
|
||||
|
||||
//go:embed src/fishbowls
|
||||
var FishbowlFS embed.FS
|
||||
|
||||
func Init() {
|
||||
Templates = make(map[string]*template.Template)
|
||||
func getTemplatesFromFS(templateFS fs.ReadDirFS) map[string]*template.Template {
|
||||
templates := make(map[string]*template.Template)
|
||||
|
||||
files := utils.Must1(templateFs.ReadDir("src"))
|
||||
files := utils.Must1(templateFS.ReadDir("src"))
|
||||
for _, f := range files {
|
||||
if hasSuffix(f.Name(), ".html") {
|
||||
t := template.New(f.Name())
|
||||
t = t.Funcs(sprig.FuncMap())
|
||||
t = t.Funcs(HMNTemplateFuncs)
|
||||
t, err := t.ParseFS(templateFs,
|
||||
t, err := t.ParseFS(templateFS,
|
||||
"src/layouts/*",
|
||||
"src/include/*",
|
||||
"src/"+f.Name(),
|
||||
|
@ -49,19 +52,40 @@ func Init() {
|
|||
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||
}
|
||||
|
||||
Templates[f.Name()] = t
|
||||
templates[f.Name()] = t
|
||||
} else if hasSuffix(f.Name(), ".css", ".js", ".xml") {
|
||||
t := template.New(f.Name())
|
||||
t = t.Funcs(sprig.FuncMap())
|
||||
t = t.Funcs(HMNTemplateFuncs)
|
||||
t, err := t.ParseFS(templateFs, "src/"+f.Name())
|
||||
t, err := t.ParseFS(templateFS, "src/"+f.Name())
|
||||
if err != nil {
|
||||
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||
}
|
||||
|
||||
Templates[f.Name()] = t
|
||||
templates[f.Name()] = t
|
||||
}
|
||||
}
|
||||
|
||||
return templates
|
||||
}
|
||||
|
||||
func Init() {
|
||||
embeddedTemplates = getTemplatesFromFS(embeddedTemplateFs)
|
||||
}
|
||||
|
||||
func GetTemplate(name string) *template.Template {
|
||||
var templates map[string]*template.Template
|
||||
if config.Config.DevConfig.LiveTemplates {
|
||||
templates = getTemplatesFromFS(utils.DirFS("src/templates").(fs.ReadDirFS))
|
||||
} else {
|
||||
templates = embeddedTemplates
|
||||
}
|
||||
|
||||
template, hasTemplate := templates[name]
|
||||
if !hasTemplate {
|
||||
panic(oops.New(nil, "Template not found: %s", name))
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func hasSuffix(s string, suffixes ...string) bool {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir.
|
||||
//
|
||||
// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the
|
||||
// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the
|
||||
// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside
|
||||
// the /prefix tree, then using DirFS does not stop the access any more than using
|
||||
// os.Open does. Additionally, the root of the fs.FS returned for a relative path,
|
||||
// DirFS("prefix"), will be affected by later calls to Chdir. DirFS is therefore not
|
||||
// a general substitute for a chroot-style security mechanism when the directory tree
|
||||
// contains arbitrary content.
|
||||
//
|
||||
// The result implements fs.StatFS AND fs.ReadDirFS because god dammit why not.
|
||||
func DirFS(dir string) fs.FS {
|
||||
return dirFS(dir)
|
||||
}
|
||||
|
||||
func containsAny(s, chars string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
for j := 0; j < len(chars); j++ {
|
||||
if s[i] == chars[j] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type dirFS string
|
||||
|
||||
var _ fs.StatFS = dirFS("")
|
||||
var _ fs.ReadDirFS = dirFS("")
|
||||
|
||||
func (dir dirFS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrInvalid}
|
||||
}
|
||||
f, err := os.Open(string(dir) + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err // nil fs.File
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
|
||||
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
|
||||
return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrInvalid}
|
||||
}
|
||||
f, err := os.Stat(string(dir) + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrInvalid}
|
||||
}
|
||||
d, err := os.ReadDir(string(dir) + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
|
@ -444,11 +444,7 @@ func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.Re
|
|||
rp.StartBlock("TEMPLATE", name)
|
||||
defer rp.EndBlock()
|
||||
}
|
||||
template, hasTemplate := templates.Templates[name]
|
||||
if !hasTemplate {
|
||||
panic(oops.New(nil, "Template not found: %s", name))
|
||||
}
|
||||
return template.Execute(rd, data)
|
||||
return templates.GetTemplate(name).Execute(rd, data)
|
||||
}
|
||||
|
||||
func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *perf.RequestPerf) {
|
||||
|
|
Loading…
Reference in New Issue