package template import ( "fmt" "html/template" "io" "io/fs" "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, fmt.Errorf("dict requires an even number of arguments") } m := make(map[string]any) for i := 0; i < len(pairs); i += 2 { key, ok := pairs[i].(string) if !ok { return nil, fmt.Errorf("dict keys must be strings") } 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 } // Key by the relative path under pages/, e.g. "auth/signin.html". relPath := strings.TrimPrefix(filePath, "pages/") tmpl, err := template.New(path.Base(filePath)).Funcs(funcMap).ParseFS(uiFS, filePath, "layouts/*.html") if err != nil { return fmt.Errorf("can't parse filesystem: %w", err) } cache[relPath] = tmpl return nil }) if err != nil { return nil, fmt.Errorf("can't walk 'pages' in '%s': %w", uiDir, err) } 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 { return fmt.Errorf("no such template: %s", pageName) } return tmpl.Execute(w, data) }