package template import ( "errors" "html/template" "io" "io/fs" "log/slog" "os" "path" "strings" ) type Service struct { baseDir string cache map[string]*template.Template } func createFuncMap() template.FuncMap { funcMap := template.FuncMap{ "dict": func(pairs ...any) (map[string]any, error) { if len(pairs)%2 != 0 { return nil, ErrDictOddArgs } m := make(map[string]any) for i := 0; i < len(pairs); i += 2 { key, ok := pairs[i].(string) if !ok { return nil, ErrDictKeyNotString } m[key] = pairs[i+1] } return m, nil }, "default": func(fallback string, value any) any { if value == nil { return fallback } return value }, } return funcMap } func NewService(uiDir string) (*Service, error) { cache := make(map[string]*template.Template) uiFS := os.DirFS(uiDir) funcMap := createFuncMap() err := fs.WalkDir(uiFS, "pages", func(filePath string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } if !strings.HasSuffix(filePath, ".html") { return nil } relPath := strings.TrimPrefix(filePath, "pages/") tmpl, err := template.New(path.Base(filePath)).Funcs(funcMap).ParseFS(uiFS, filePath, "layouts/*.html") if err != nil { slog.Info("parse templates failed", "file", filePath, "error", err) return ErrParse } cache[relPath] = tmpl return nil }) if err != nil { if errors.Is(err, ErrParse) { return nil, ErrParse } slog.Info("walk pages failed", "dir", uiDir, "error", err) return nil, ErrWalk } return &Service{ baseDir: uiDir, cache: cache, }, nil } func (e *Service) Render(w io.Writer, pageName string, data any) error { tmpl, ok := e.cache[pageName] if !ok { slog.Info("no such template", "page", pageName) return ErrNoSuchTemplate } if err := tmpl.Execute(w, data); err != nil { slog.Info("render template failed", "page", pageName, "error", err) return ErrRender } return nil }