package web import ( "context" "fmt" "html/template" "io/fs" "net/http" "path" "strings" "time" content "inventree/web" ) // templateFuncs are available to every template (pages and fragments). var templateFuncs = template.FuncMap{ "dict": func(pairs ...any) (map[string]any, error) { if len(pairs)%2 != 0 { return nil, fmt.Errorf("dict: odd number of arguments") } m := make(map[string]any, len(pairs)/2) for i := 0; i < len(pairs); i += 2 { key, ok := pairs[i].(string) if !ok { return nil, fmt.Errorf("dict: key %v is not a string", pairs[i]) } m[key] = pairs[i+1] } return m, nil }, "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "mul": func(a, b int) int { return a * b }, "ceilDiv": func(a, b int) int { if b <= 0 { return 0 } return (a + b - 1) / b }, "seq": func(n int) []int { out := make([]int, n) for i := range out { out[i] = i } return out }, "seqFrom1": func(n int) []int { out := make([]int, n) for i := range out { out[i] = i + 1 } return out }, "fmtDate": func(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02 15:04") }, "fmtDateShort": func(t time.Time) string { if t.IsZero() { return "" } return t.Format("Jan 2, 2006") }, "iconOr": func(icon, fallback string) string { if icon == "" { return fallback } return icon }, "hasPrefix": strings.HasPrefix, "lower": strings.ToLower, "list": func(items ...string) []string { return items }, "deref": func(p *int64) int64 { if p == nil { return 0 } return *p }, } // templateSet bundles the shared fragment/partial template collection with // one isolated template set per full page (see loadTemplates for why pages // must be isolated from one another). type templateSet struct { shared *template.Template // layout.html + partials/*.html; used to render AJAX fragments directly pages map[string]*template.Template // page name -> shared clone + that page's templates/pages/.html login *template.Template // standalone minimal layout for the login screen } // loadTemplates parses layout.html and every partial once into a shared // template set, then clones that set once per page in templates/pages/ so // each page can define a block named "content" without colliding with the // others (html/template templates sharing one *template.Template must have // unique names, but isolated clones may reuse the same name freely). func loadTemplates(a *App) (*templateSet, error) { // siteName is bound per-App (rather than living in the package-level // templateFuncs) since it needs a live DB read through a.Settings; it's // used by layout.html for the and navbar brand text. appFuncs := template.FuncMap{ "siteName": func() string { name, err := a.Settings.Get(context.Background(), "site_name", "inventree") if err != nil || name == "" { return "inventree" } return name }, } shared := template.New("root").Funcs(templateFuncs).Funcs(appFuncs) shared, err := shared.ParseFS(content.TemplatesFS, "templates/layout.html") if err != nil { return nil, fmt.Errorf("parse layout: %w", err) } shared, err = parseGlob(shared, "templates/partials") if err != nil { return nil, fmt.Errorf("parse partials: %w", err) } pageFiles, err := fs.Glob(content.TemplatesFS, "templates/pages/*.html") if err != nil { return nil, err } pages := map[string]*template.Template{} for _, pf := range pageFiles { name := strings.TrimSuffix(path.Base(pf), ".html") clone, err := shared.Clone() if err != nil { return nil, fmt.Errorf("clone template set for %s: %w", name, err) } if _, err := clone.ParseFS(content.TemplatesFS, pf); err != nil { return nil, fmt.Errorf("parse page %s: %w", pf, err) } pages[name] = clone } login := template.New("login-root").Funcs(templateFuncs).Funcs(appFuncs) login, err = login.ParseFS(content.TemplatesFS, "templates/login.html") if err != nil { return nil, fmt.Errorf("parse login: %w", err) } return &templateSet{shared: shared, pages: pages, login: login}, nil } func parseGlob(t *template.Template, dir string) (*template.Template, error) { files, err := fs.Glob(content.TemplatesFS, dir+"/*.html") if err != nil { return nil, err } if len(files) == 0 { return t, nil } return t.ParseFS(content.TemplatesFS, files...) } // Render executes the named page's "layout" template (which pulls in that // page's "content" block) into the response. Page "login" uses its own // minimal "login" template instead. func (a *App) Render(w http.ResponseWriter, status int, page string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if page == "login" { w.WriteHeader(status) if err := a.templates.login.ExecuteTemplate(w, "login", data); err != nil { a.Log.Error("template render failed", "page", page, "error", err) } return } ts, ok := a.templates.pages[page] if !ok { writeErr(w, http.StatusInternalServerError, "template not found: "+page) return } w.WriteHeader(status) if err := ts.ExecuteTemplate(w, "layout", data); err != nil { a.Log.Error("template render failed", "page", page, "error", err) } } // RenderFragment executes a single named partial (defined under // templates/partials/) with no layout wrapper, for AJAX responses. func (a *App) RenderFragment(w http.ResponseWriter, status int, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) if err := a.templates.shared.ExecuteTemplate(w, name, data); err != nil { a.Log.Error("fragment render failed", "name", name, "error", err) } }