Files
rndr/renderer.go
T
2026-10-02 21:04:17 +02:00

103 lines
2.6 KiB
Go

package rndr
import (
"html/template"
"io/fs"
"net/http"
"path/filepath"
"reflect"
"strings"
"unicode"
log "gitlab.com/gdulai/simpleloglvl"
)
// ComponentRenderer handles the associations between templates and models, and renders the template files with the appropriate backing model.
type ComponentRenderer struct {
templates *template.Template
}
// NewRenderer creates returns the pointer of newly created ComponentRenderer, or an error.
func NewRenderer(root string) (*ComponentRenderer, error) {
templates, err := loadTemplates(root)
if err != nil {
return nil, err
}
return &ComponentRenderer{templates: templates}, nil
}
// Render, renders a template html file with a backing model.
// The template file name is derived from the model type via reflection.
func (renderer *ComponentRenderer) Render(w http.ResponseWriter, r *http.Request, model any) {
requestId := r.Context().Value("requestId")
modelTyp := reflect.TypeOf(model)
for modelTyp.Kind() == reflect.Pointer {
modelTyp = modelTyp.Elem()
}
if modelTyp == nil {
log.LogError("Could not determine model type for %s (%s)", model, requestId)
return
}
templateName := toKebabCase(modelTyp.Name()) + ".html"
w.Header().Set("Content-Type", "text/html")
err := renderer.templates.ExecuteTemplate(w, templateName, model)
if err != nil {
log.LogError("Error while serving html teplate: %s (%s)", err, requestId)
http.Error(w, "Component render error", http.StatusInternalServerError)
}
}
// RenderLayout, renders layout template file, without any backing model
func (renderer *ComponentRenderer) RenderLayout(w http.ResponseWriter, r *http.Request, templateName string) {
requestId := r.Context().Value("requestId")
err := renderer.templates.ExecuteTemplate(w, templateName, nil)
if err != nil {
log.LogError("Error while serving html teplate: %s (%s)", err, requestId)
http.Error(w, "Component render error", http.StatusInternalServerError)
}
}
func loadTemplates(root string) (*template.Template, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".html" {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, err
}
return template.ParseFiles(files...)
}
func toKebabCase(s string) string {
var b strings.Builder
for i, r := range s {
if i > 0 && unicode.IsUpper(r) {
prev := rune(s[i-1])
if unicode.IsLower(prev) ||
(i+1 < len(s) && unicode.IsLower(rune(s[i+1]))) {
b.WriteByte('-')
}
}
b.WriteRune(unicode.ToLower(r))
}
return b.String()
}