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 {
|
if cfg.CreateTemplates {
|
||||||
slog.Debug("creating templates")
|
slog.Debug("creating templates")
|
||||||
app.Templates = NewHTMLRender()
|
app.Templates = NewHTMLRender()
|
||||||
|
|
||||||
|
if cfg.EnvMode == EnvironmentProduction {
|
||||||
|
app.Templates.EnableCache = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CreateSession {
|
if cfg.CreateSession {
|
||||||
|
|||||||
22
broker.go
22
broker.go
@ -1,10 +1,13 @@
|
|||||||
package goblocks
|
package goblocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -129,17 +132,13 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case message := <-client.Send:
|
case message := <-client.Send:
|
||||||
|
slog.Info("message", "message", message)
|
||||||
var data string
|
var data string
|
||||||
switch v := message.Data.(type) {
|
switch v := message.Data.(type) {
|
||||||
case string:
|
case string:
|
||||||
data = v
|
data = v
|
||||||
case map[string]any, []any:
|
case template.HTML:
|
||||||
jsonData, err := json.Marshal(v)
|
data = string(v)
|
||||||
if err != nil {
|
|
||||||
slog.Error("error marshaling message", "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data = string(jsonData)
|
|
||||||
default:
|
default:
|
||||||
jsonData, err := json.Marshal(v)
|
jsonData, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -148,7 +147,14 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
data = string(jsonData)
|
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:
|
case <-client.Close:
|
||||||
slog.Info("client closed", "client_id", clientID)
|
slog.Info("client closed", "client_id", clientID)
|
||||||
return
|
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)
|
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.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
err := a.Templates.Template(w, name, td)
|
err := a.Templates.Template(w, layout, page, td)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error rendering template", "error", err)
|
slog.Error("error rendering template", "error", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
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) {
|
func (a *App) RenderComponent(name string, td *TemplateData) (string, error) {
|
||||||
sw := newStringWriter()
|
result, err := a.Templates.RenderComponent(name, td)
|
||||||
err := a.Templates.Template(sw, name, td)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error rendering template", "error", err)
|
slog.Error("error rendering component", "component", name, "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return sw.builder.String(), nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
28
router.go
28
router.go
@ -2,7 +2,10 @@ package goblocks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Middleware func(http.Handler) http.Handler
|
type Middleware func(http.Handler) http.Handler
|
||||||
@ -35,6 +38,31 @@ func (r *Router) Group(fn func(r *Router)) {
|
|||||||
fn(sub)
|
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) {
|
func (r *Router) HandleFunc(pattern string, h http.HandlerFunc) {
|
||||||
r.Handle(pattern, h)
|
r.Handle(pattern, h)
|
||||||
}
|
}
|
||||||
|
|||||||
127
templates.go
127
templates.go
@ -4,8 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -37,7 +37,10 @@ func defaultHTMLRender() *Render {
|
|||||||
EnableCache: false,
|
EnableCache: false,
|
||||||
TemplatesPath: "templates",
|
TemplatesPath: "templates",
|
||||||
TemplateData: TemplateData{},
|
TemplateData: TemplateData{},
|
||||||
Functions: template.FuncMap{},
|
Functions: template.FuncMap{
|
||||||
|
"default": defaultIfEmpty,
|
||||||
|
"dict": Dict,
|
||||||
|
},
|
||||||
templateCache: templateCache{},
|
templateCache: templateCache{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,117 +56,99 @@ func (re *Render) apply(opts ...RenderOptions) *Render {
|
|||||||
opt(re)
|
opt(re)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return re
|
return re
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultIfEmpty(fallback, value string) string {
|
func defaultIfEmpty(fallback string, value any) string {
|
||||||
if strings.TrimSpace(value) == "" {
|
str, ok := value.(string)
|
||||||
|
if !ok || strings.TrimSpace(str) == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
func (re *Render) Template(w http.ResponseWriter, tmpl string, td *TemplateData) error {
|
func cloneFuncMap(src template.FuncMap) template.FuncMap {
|
||||||
var tc templateCache
|
c := make(template.FuncMap)
|
||||||
var err error
|
maps.Copy(c, src)
|
||||||
|
return c
|
||||||
re.Functions["default"] = defaultIfEmpty
|
}
|
||||||
|
|
||||||
|
func (re *Render) Template(w http.ResponseWriter, layoutName, pageName string, td *TemplateData) error {
|
||||||
if td == nil {
|
if td == nil {
|
||||||
td = &TemplateData{}
|
td = &TemplateData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
tc, err = re.getTemplateCache()
|
tmpl, err := re.loadTemplateWithLayout(layoutName, pageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t, ok := tc[tmpl]
|
|
||||||
if !ok {
|
|
||||||
return errors.New("can't get template from cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = buf.WriteTo(w); err != nil {
|
_, err = buf.WriteTo(w)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
func (re *Render) RenderComponent(name string, td *TemplateData) (string, error) {
|
||||||
|
if td == nil {
|
||||||
|
td = &TemplateData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (re *Render) getTemplateCache() (templateCache, error) {
|
path := filepath.Join(re.TemplatesPath, "components", name)
|
||||||
if len(re.templateCache) == 0 {
|
|
||||||
cachedTemplates, err := re.createTemplateCache()
|
files := []string{path}
|
||||||
|
matches, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
re.templateCache = cachedTemplates
|
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 {
|
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) {
|
layoutPath := filepath.Join(re.TemplatesPath, "layouts", layoutName)
|
||||||
var files []string
|
pagePath := filepath.Join(re.TemplatesPath, "pages", pageName)
|
||||||
|
|
||||||
err := filepath.WalkDir(re.TemplatesPath, func(path string, d fs.DirEntry, err error) error {
|
files := []string{layoutPath, pagePath}
|
||||||
|
componentFiles, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
files = append(files, componentFiles...)
|
||||||
|
|
||||||
if !d.IsDir() && filepath.Ext(path) == ".gohtml" {
|
funcs := cloneFuncMap(re.Functions)
|
||||||
files = append(files, path)
|
tmpl, err := template.New(layoutName).Funcs(funcs).ParseFiles(files...)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return files, nil
|
if re.EnableCache {
|
||||||
|
re.templateCache[cacheKey] = tmpl
|
||||||
|
slog.Debug("template cached", "key", cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (re *Render) createTemplateCache() (templateCache, error) {
|
return tmpl, nil
|
||||||
cache := templateCache{}
|
|
||||||
var baseTemplates []string
|
|
||||||
var renderTemplates []string
|
|
||||||
|
|
||||||
templates, err := re.findHTMLFiles()
|
|
||||||
if err != nil {
|
|
||||||
return cache, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pages contains pagination info.
|
// Pages contains pagination info.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user