/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package template import ( "fmt" "html/template" "io" "math" "net/http" "net/url" "path" "strconv" "strings" "time" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/number" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/build" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" ) func adminTemplateFile(name string) string { return "web/templates/admin/" + name } func publicTemplateFile(name string) string { return "web/templates/public/" + name } func MustRenderAdmin(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) { layout := "layout.gohtml" if httplib.IsHTMxRequest(r) { layout = "htmx.gohtml" } mustRenderLayout(w, user, company, adminTemplateFile, data, layout, filename) } func MustRenderAdminFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) { layout := "layout.gohtml" if httplib.IsHTMxRequest(r) { layout = "htmx.gohtml" } filenames = append([]string{layout}, filenames...) mustRenderLayout(w, user, company, adminTemplateFile, data, filenames...) } func MustRenderAdminNoLayout(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) { mustRenderLayout(w, user, company, adminTemplateFile, data, filename) } func MustRenderAdminNoLayoutFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) { mustRenderLayout(w, user, company, adminTemplateFile, data, filenames...) } func MustRenderPublic(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) { layout := "layout.gohtml" mustRenderLayout(w, user, company, publicTemplateFile, data, layout, filename) } func MustRenderPublicNoLayout(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) { mustRenderLayout(w, user, company, publicTemplateFile, data, filename) } func MustRenderPublicFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) { layout := "layout.gohtml" filenames = append([]string{layout}, filenames...) mustRenderLayout(w, user, company, publicTemplateFile, data, filenames...) } func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templateFile func(string) string, data interface{}, templates ...string) { t := template.New(templates[len(templates)-1]) t.Funcs(template.FuncMap{ "camperVersion": func() string { return build.Version }, "gettext": user.Locale.Get, "pgettext": user.Locale.GetC, "currentLocale": func() string { return user.Locale.Language.String() }, "isLoggedIn": func() bool { return user.LoggedIn }, "isAdmin": user.IsAdmin, "CSRFHeader": func() string { return fmt.Sprintf(`"%s": "%s"`, auth.CSRFTokenHeader, user.CSRFToken) }, "CSRFInput": func() template.HTML { return template.HTML(fmt.Sprintf(``, auth.CSRFTokenField, user.CSRFToken)) }, "raw": func(s string) template.HTML { return template.HTML(s) }, "replaceAll": func(s, old, new string) string { return strings.ReplaceAll(s, old, new) }, "humanizeBytes": func(bytes int64) string { return humanizeBytes(bytes) }, "formatPrice": func(price string) string { return FormatPrice(price, user.Locale.Language, user.Locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol) }, "formatDate": func(time time.Time) template.HTML { return template.HTML(`") }, "formatDateTime": func(time time.Time) template.HTML { return template.HTML(`") }, "formatDateAttr": func(time time.Time) string { return time.Format(database.ISODateFormat) }, "formatPercent": func(value int) string { return fmt.Sprintf("%d %%", value) }, "today": func() string { return time.Now().Format(database.ISODateFormat) }, "queryEscape": func(s string) string { return url.QueryEscape(s) }, "inc": func(i int) int { return i + 1 }, "dec": func(i int) int { return i - 1 }, "add": func(y, x int) int { return x + y }, "sub": func(y, x int) int { return x - y }, "int": func(v interface{}) int { switch v := v.(type) { case int: return v case bool: if v { return 1 } else { return 0 } case time.Weekday: return int(v) case time.Month: return int(v) default: panic(fmt.Errorf("could not convert to integer")) } }, "hexToDec": func(s string) int { num, _ := strconv.ParseInt(s, 16, 0) return int(num) }, "slugify": Slugify, "colspan": func(colspan int, cursor *form.Cursor) *form.Cursor { cursor.Colspan = colspan return cursor }, }) templates = append(templates, "form.gohtml") files := make([]string, len(templates)) for i, tmpl := range templates { if len(tmpl) > 4 && tmpl[0] == 'w' && tmpl[1] == 'e' && tmpl[2] == 'b' && tmpl[3] == '/' { files[i] = tmpl } else { files[i] = templateFile(tmpl) } } if _, err := t.ParseFiles(files...); err != nil { panic(err) } if rw, ok := w.(http.ResponseWriter); ok { rw.Header().Set("Content-Type", "text/html; charset=utf-8") } if err := t.ExecuteTemplate(w, path.Base(templates[0]), data); err != nil { panic(err) } } func FormatPrice(price string, language language.Tag, currencyPattern string, decimalDigits int, currencySymbol string) string { p := message.NewPrinter(language) f, err := strconv.ParseFloat(price, 64) if err != nil { f = math.NaN() } return p.Sprintf(currencyPattern, decimalDigits, number.Decimal(f), currencySymbol) }