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() }