fixes and improvements in broker and render
This commit is contained in:
parent
ba604bb8b4
commit
2fea2e6ac5
4
app.go
4
app.go
@ -226,6 +226,10 @@ func NewApp(config ...Config) *App {
|
||||
if cfg.CreateTemplates {
|
||||
slog.Debug("creating templates")
|
||||
app.Templates = NewHTMLRender()
|
||||
|
||||
if cfg.EnvMode == EnvironmentProduction {
|
||||
app.Templates.EnableCache = true
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.CreateSession {
|
||||
|
||||
22
broker.go
22
broker.go
@ -1,10 +1,13 @@
|
||||
package goblocks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -129,17 +132,13 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
for {
|
||||
select {
|
||||
case message := <-client.Send:
|
||||
slog.Info("message", "message", message)
|
||||
var data string
|
||||
switch v := message.Data.(type) {
|
||||
case string:
|
||||
data = v
|
||||
case map[string]any, []any:
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
slog.Error("error marshaling message", "error", err)
|
||||
continue
|
||||
}
|
||||
data = string(jsonData)
|
||||
case template.HTML:
|
||||
data = string(v)
|
||||
default:
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
@ -148,7 +147,14 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
data = string(jsonData)
|
||||
}
|
||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", message.Event, data)
|
||||
fmt.Fprintf(w, "event: %s\n", message.Event)
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
fmt.Fprintf(w, "data: %s\n", scanner.Text())
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "\n")
|
||||
case <-client.Close:
|
||||
slog.Info("client closed", "client_id", clientID)
|
||||
return
|
||||
|
||||
13
render.go
13
render.go
@ -34,10 +34,10 @@ func (a *App) JSON(w http.ResponseWriter, code int, v any) {
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func (a *App) HTML(w http.ResponseWriter, code int, name string, td *TemplateData) {
|
||||
func (a *App) HTML(w http.ResponseWriter, code int, layout, page string, td *TemplateData) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
err := a.Templates.Template(w, name, td)
|
||||
err := a.Templates.Template(w, layout, page, td)
|
||||
if err != nil {
|
||||
slog.Error("error rendering template", "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@ -45,12 +45,11 @@ func (a *App) HTML(w http.ResponseWriter, code int, name string, td *TemplateDat
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) RenderHTML(name string, td *TemplateData) (string, error) {
|
||||
sw := newStringWriter()
|
||||
err := a.Templates.Template(sw, name, td)
|
||||
func (a *App) RenderComponent(name string, td *TemplateData) (string, error) {
|
||||
result, err := a.Templates.RenderComponent(name, td)
|
||||
if err != nil {
|
||||
slog.Error("error rendering template", "error", err)
|
||||
slog.Error("error rendering component", "component", name, "error", err)
|
||||
return "", err
|
||||
}
|
||||
return sw.builder.String(), nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
28
router.go
28
router.go
@ -2,7 +2,10 @@ package goblocks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
@ -35,6 +38,31 @@ func (r *Router) Group(fn func(r *Router)) {
|
||||
fn(sub)
|
||||
}
|
||||
|
||||
func (r *Router) Static(urlPrefix, dir string) {
|
||||
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
|
||||
|
||||
fileServer := http.FileServer(http.Dir(dir))
|
||||
fs := http.StripPrefix(urlPrefix, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
fullPath := filepath.Join(dir, req.URL.Path)
|
||||
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
|
||||
fileServer.ServeHTTP(w, req)
|
||||
}))
|
||||
|
||||
r.Handle(urlPrefix+"/", fs)
|
||||
}
|
||||
|
||||
func (r *Router) HandleFunc(pattern string, h http.HandlerFunc) {
|
||||
r.Handle(pattern, h)
|
||||
}
|
||||
|
||||
143
templates.go
143
templates.go
@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@ -37,7 +37,10 @@ func defaultHTMLRender() *Render {
|
||||
EnableCache: false,
|
||||
TemplatesPath: "templates",
|
||||
TemplateData: TemplateData{},
|
||||
Functions: template.FuncMap{},
|
||||
Functions: template.FuncMap{
|
||||
"default": defaultIfEmpty,
|
||||
"dict": Dict,
|
||||
},
|
||||
templateCache: templateCache{},
|
||||
}
|
||||
}
|
||||
@ -53,117 +56,99 @@ func (re *Render) apply(opts ...RenderOptions) *Render {
|
||||
opt(re)
|
||||
}
|
||||
}
|
||||
|
||||
return re
|
||||
}
|
||||
|
||||
func defaultIfEmpty(fallback, value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
func defaultIfEmpty(fallback string, value any) string {
|
||||
str, ok := value.(string)
|
||||
if !ok || strings.TrimSpace(str) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (re *Render) Template(w http.ResponseWriter, tmpl string, td *TemplateData) error {
|
||||
var tc templateCache
|
||||
var err error
|
||||
|
||||
re.Functions["default"] = defaultIfEmpty
|
||||
func cloneFuncMap(src template.FuncMap) template.FuncMap {
|
||||
c := make(template.FuncMap)
|
||||
maps.Copy(c, src)
|
||||
return c
|
||||
}
|
||||
|
||||
func (re *Render) Template(w http.ResponseWriter, layoutName, pageName string, td *TemplateData) error {
|
||||
if td == nil {
|
||||
td = &TemplateData{}
|
||||
}
|
||||
|
||||
tc, err = re.getTemplateCache()
|
||||
tmpl, err := re.loadTemplateWithLayout(layoutName, pageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, ok := tc[tmpl]
|
||||
if !ok {
|
||||
return errors.New("can't get template from cache")
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err = t.Execute(buf, td); err != nil {
|
||||
if err = tmpl.ExecuteTemplate(buf, strings.TrimSuffix(layoutName, ".gohtml"), td); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = buf.WriteTo(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
func (re *Render) getTemplateCache() (templateCache, error) {
|
||||
if len(re.templateCache) == 0 {
|
||||
cachedTemplates, err := re.createTemplateCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re.templateCache = cachedTemplates
|
||||
func (re *Render) RenderComponent(name string, td *TemplateData) (string, error) {
|
||||
if td == nil {
|
||||
td = &TemplateData{}
|
||||
}
|
||||
|
||||
path := filepath.Join(re.TemplatesPath, "components", name)
|
||||
|
||||
files := []string{path}
|
||||
matches, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
files = append(files, matches...)
|
||||
|
||||
funcs := cloneFuncMap(re.Functions)
|
||||
tmpl, err := template.New(name).Funcs(funcs).ParseFiles(files...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, strings.TrimSuffix(name, ".gohtml"), td)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func (re *Render) loadTemplateWithLayout(layoutName, pageName string) (*template.Template, error) {
|
||||
cacheKey := layoutName + "::" + pageName
|
||||
|
||||
if re.EnableCache {
|
||||
return re.templateCache, nil
|
||||
if tmpl, ok := re.templateCache[cacheKey]; ok {
|
||||
return tmpl, nil
|
||||
}
|
||||
}
|
||||
return re.createTemplateCache()
|
||||
}
|
||||
|
||||
func (re *Render) findHTMLFiles() ([]string, error) {
|
||||
var files []string
|
||||
layoutPath := filepath.Join(re.TemplatesPath, "layouts", layoutName)
|
||||
pagePath := filepath.Join(re.TemplatesPath, "pages", pageName)
|
||||
|
||||
err := filepath.WalkDir(re.TemplatesPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !d.IsDir() && filepath.Ext(path) == ".gohtml" {
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
files := []string{layoutPath, pagePath}
|
||||
componentFiles, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, componentFiles...)
|
||||
|
||||
funcs := cloneFuncMap(re.Functions)
|
||||
tmpl, err := template.New(layoutName).Funcs(funcs).ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (re *Render) createTemplateCache() (templateCache, error) {
|
||||
cache := templateCache{}
|
||||
var baseTemplates []string
|
||||
var renderTemplates []string
|
||||
|
||||
templates, err := re.findHTMLFiles()
|
||||
if err != nil {
|
||||
return cache, err
|
||||
if re.EnableCache {
|
||||
re.templateCache[cacheKey] = tmpl
|
||||
slog.Debug("template cached", "key", cacheKey)
|
||||
}
|
||||
|
||||
slog.Debug("templates", "templates", templates)
|
||||
|
||||
for _, file := range templates {
|
||||
filePathBase := filepath.Base(file)
|
||||
if strings.Contains(filePathBase, "layout") || strings.Contains(filePathBase, "fragment") {
|
||||
baseTemplates = append(baseTemplates, file)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range templates {
|
||||
filePathBase := filepath.Base(file)
|
||||
if strings.Contains(filePathBase, "page") || strings.Contains(filePathBase, "component") {
|
||||
renderTemplates = append(baseTemplates, file)
|
||||
ts, err := template.New(filePathBase).Funcs(re.Functions).ParseFiles(append(baseTemplates, renderTemplates...)...)
|
||||
if err != nil {
|
||||
return cache, err
|
||||
}
|
||||
cache[filePathBase] = ts
|
||||
}
|
||||
}
|
||||
|
||||
return cache, nil
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// Pages contains pagination info.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user