diff --git a/cmd/cmd.go b/cmd/cmd.go index 4b221e5..41b6417 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -14,6 +14,7 @@ import ( "syscall" comicverse "forge.capytal.company/capytalcode/project-comicverse" + "forge.capytal.company/capytalcode/project-comicverse/templates" "forge.capytal.company/loreddev/x/tinyssert" "github.com/aws/aws-sdk-go-v2/aws" @@ -112,6 +113,9 @@ func main() { d := os.DirFS("./assets") opts = append(opts, comicverse.WithAssets(d)) + t := templates.NewHotTemplates(os.DirFS("./templates")) + opts = append(opts, comicverse.WithTemplates(t)) + opts = append(opts, comicverse.WithDevelopmentMode()) } diff --git a/comicverse.go b/comicverse.go index 019fd59..d7edb0e 100644 --- a/comicverse.go +++ b/comicverse.go @@ -26,6 +26,7 @@ func New(cfg Config, opts ...Option) (http.Handler, error) { bucket: cfg.Bucket, assets: assets.Files(), + templates: templates.Templates(), developmentMode: false, ctx: context.Background(), @@ -50,6 +51,9 @@ func New(cfg Config, opts ...Option) (http.Handler, error) { if app.assets == nil { return nil, errors.New("static files must not be a nil interface") } + if app.templates == nil { + return nil, errors.New("templates must not be a nil interface") + } if app.ctx == nil { return nil, errors.New("context must not be a nil interface") @@ -82,6 +86,10 @@ func WithAssets(f fs.FS) Option { return func(app *app) { app.assets = joinedfs.Join(f, app.assets) } } +func WithTemplates(t templates.ITemplate) Option { + return func(app *app) { app.templates = t } +} + func WithAssertions(a tinyssert.Assertions) Option { return func(app *app) { app.assert = a } } @@ -102,6 +110,7 @@ type app struct { ctx context.Context assets fs.FS + templates templates.ITemplate developmentMode bool handler http.Handler @@ -147,9 +156,9 @@ func (app *app) setup() error { app.handler, err = router.New(router.Config{ Service: service, - Templates: templates.Templates(), + Templates: app.templates, DisableCache: app.developmentMode, - Assets: app.assets, + Assets: app.assets, Assertions: app.assert, Logger: app.logger.WithGroup("router"), diff --git a/makefile b/makefile index 1c8dbbe..b82c19f 100644 --- a/makefile +++ b/makefile @@ -13,7 +13,7 @@ dev/server: --build.cmd "go build -o tmp/bin/main ./cmd" \ --build.bin "tmp/bin/main" \ --build.exclude_dir "node_modules" \ - --build.include_ext "go,html" \ + --build.include_ext "go" \ --build.stop_on_error "false" \ --misc.clean_on_exit true \ -- -dev -port $(PORT) -hostname 0.0.0.0 diff --git a/router/router.go b/router/router.go index 9d66f7f..ab82f5b 100644 --- a/router/router.go +++ b/router/router.go @@ -2,12 +2,12 @@ package router import ( "errors" - "html/template" "io/fs" "log/slog" "net/http" "forge.capytal.company/capytalcode/project-comicverse/service" + "forge.capytal.company/capytalcode/project-comicverse/templates" "forge.capytal.company/loreddev/x/smalltrip" "forge.capytal.company/loreddev/x/smalltrip/exception" "forge.capytal.company/loreddev/x/smalltrip/middleware" @@ -17,7 +17,7 @@ import ( type router struct { service *service.Service - templates *template.Template + templates templates.ITemplate assets fs.FS cache bool @@ -59,7 +59,7 @@ func New(cfg Config) (http.Handler, error) { type Config struct { Service *service.Service - Templates *template.Template + Templates templates.ITemplate Assets fs.FS DisableCache bool diff --git a/templates/templates.go b/templates/templates.go index d87cfbb..3a80243 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,39 +1,77 @@ package templates +// INFO: This will probably become a new lib in loreddev/x at some point + import ( "embed" "errors" "fmt" "html/template" + "io" + "io/fs" +) + +var ( + patterns = []string{"*.html", "layouts/*.html"} + functions = template.FuncMap{ + "args": func(pairs ...any) (map[string]any, error) { + if len(pairs)%2 != 0 { + return nil, errors.New("misaligned map in template arguments") + } + + m := make(map[string]any, len(pairs)/2) + + for i := 0; i < len(pairs); i += 2 { + key, ok := pairs[i].(string) + if !ok { + return nil, fmt.Errorf("cannot use type %T as map key", pairs[i]) + } + + m[key] = pairs[i+1] + } + + return m, nil + }, + } ) //go:embed *.html layouts/*.html var embedded embed.FS -var temps = template.Must(template.New("templates").Funcs(template.FuncMap{ - "args": func(pairs ...any) (map[string]any, error) { - if len(pairs)%2 != 0 { - return nil, errors.New("misaligned map in template arguments") - } - - m := make(map[string]any, len(pairs)/2) - - for i := 0; i < len(pairs); i += 2 { - key, ok := pairs[i].(string) - if !ok { - return nil, fmt.Errorf("cannot use type %T as map key", pairs[i]) - } - - m[key] = pairs[i+1] - } - - return m, nil - }, -}).ParseFS(embedded, - "*.html", - "layouts/*.html", -)) +var temps = template.Must(template.New("templates").Funcs(functions).ParseFS(embedded, patterns...)) func Templates() *template.Template { - return temps + return temps // TODO: Support for local templates/hot-reloading without rebuild +} + +func NewHotTemplates(fsys fs.FS) *HotTemplate { + return &HotTemplate{ + fs: fsys, + } +} + +type HotTemplate struct { + fs fs.FS + template *template.Template +} + +func (t *HotTemplate) Execute(wr io.Writer, data any) error { + te, err := template.New("hot-templates").Funcs(functions).ParseFS(t.fs, patterns...) + if err != nil { + return err + } + return te.Execute(wr, data) +} + +func (t *HotTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error { + te, err := template.New("hot-templates").Funcs(functions).ParseFS(t.fs, patterns...) + if err != nil { + return err + } + return te.ExecuteTemplate(wr, name, data) +} + +type ITemplate interface { + Execute(wr io.Writer, data any) error + ExecuteTemplate(wr io.Writer, name string, data any) error }