package templates import ( "bytes" "encoding/json" "fmt" "html/template" "net/http" "net/url" "sort" "strings" "time" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/markdown" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/photo" "code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/utility" "golang.org/x/text/language" "golang.org/x/text/message" ) // Generics type Number interface { int | int64 | uint64 | float32 | float64 } // TemplateFuncs available to all pages. func TemplateFuncs(r *http.Request) template.FuncMap { return template.FuncMap{ "InputCSRF": InputCSRF(r), "SincePrettyCoarse": SincePrettyCoarse(), "FormatNumberShort": FormatNumberShort(), "FormatNumberCommas": FormatNumberCommas(), "ComputeAge": utility.Age, "Split": strings.Split, "ToMarkdown": ToMarkdown, "ToJSON": ToJSON, "ToHTML": ToHTML, "PhotoURL": photo.URLPath, "Now": time.Now, "RunTime": RunTime, "PrettyTitle": func() template.HTML { return template.HTML(fmt.Sprintf( `non` + `shy`, )) }, "PrettyTitleShort": func() template.HTML { return template.HTML(`n` + `s`, ) }, "PrettyCircle": func() template.HTML { return template.HTML( `Inner circle`, ) }, "Pluralize": Pluralize[int], "Pluralize64": Pluralize[int64], "PluralizeU64": Pluralize[uint64], "Substring": Substring, "TrimEllipses": TrimEllipses, "IterRange": IterRange, "SubtractInt": SubtractInt, "UrlEncode": UrlEncode, "QueryPlus": QueryPlus(r), "SimplePager": SimplePager(r), "HasSuffix": strings.HasSuffix, // Test if a photo should be blurred ({{BlurExplicit .Photo}}) "BlurExplicit": BlurExplicit(r), // Get a description for an admin scope (e.g. for transparency page). "AdminScopeDescription": config.AdminScopeDescription, } } // InputCSRF returns the HTML snippet for a CSRF token hidden input field. func InputCSRF(r *http.Request) func() template.HTML { return func() template.HTML { ctx := r.Context() if token, ok := ctx.Value(session.CSRFKey).(string); ok { return template.HTML(fmt.Sprintf( ``, config.CSRFInputName, token, )) } else { return template.HTML(`[CSRF middleware error]`) } } } // RunTime returns the elapsed time between the HTTP request start and now, as a formatted string. func RunTime(r *http.Request) string { if rt, ok := r.Context().Value(session.RequestTimeKey).(time.Time); ok { duration := time.Since(rt) return duration.Round(time.Millisecond).String() } return "ERROR" } // BlurExplicit returns true if the current user has the blur_explicit setting on and the given Photo is Explicit. func BlurExplicit(r *http.Request) func(*models.Photo) bool { return func(photo *models.Photo) bool { if !photo.Explicit { return false } currentUser, err := session.CurrentUser(r) if err != nil { return false } return currentUser.GetProfileField("blur_explicit") == "true" } } // SincePrettyCoarse formats a time.Duration in plain English. Intended for "joined 2 months ago" type // strings - returns the coarsest level of granularity. func SincePrettyCoarse() func(time.Time) template.HTML { return func(since time.Time) template.HTML { return template.HTML(utility.FormatDurationCoarse(time.Since(since))) } } // FormatNumberShort will format any integer number into a short representation. func FormatNumberShort() func(v interface{}) template.HTML { return func(v interface{}) template.HTML { var number int64 switch t := v.(type) { case int: number = int64(t) case int64: number = int64(t) case uint: number = int64(t) case uint64: number = int64(t) case float32: number = int64(t) case float64: number = int64(t) default: return template.HTML("#INVALID#") } return template.HTML(utility.FormatNumberShort(number)) } } // FormatNumberCommas will pretty print a long number by adding commas. func FormatNumberCommas() func(v interface{}) template.HTML { return func(v interface{}) template.HTML { var number int64 switch t := v.(type) { case int: number = int64(t) case int64: number = int64(t) case uint: number = int64(t) case uint64: number = int64(t) case float32: number = int64(t) case float64: number = int64(t) default: return template.HTML("#INVALID#") } p := message.NewPrinter(language.English) return template.HTML(p.Sprintf("%d", number)) } } // ToMarkdown renders input text as Markdown. func ToMarkdown(input string) template.HTML { return template.HTML(markdown.Render(input)) } // ToHTML renders input text as trusted HTML code. func ToHTML(input string) template.HTML { return template.HTML(input) } // ToJSON will stringify any json-serializable object. func ToJSON(v any) template.JS { bin, err := json.Marshal(v) if err != nil { return template.JS(err.Error()) } return template.JS(string(bin)) } // Pluralize text based on a quantity number. Provide up to 2 labels for the // singular and plural cases, or the defaults are "", "s" func Pluralize[V Number](count V, labels ...string) string { if len(labels) < 2 { labels = []string{"", "s"} } if count == 1 { return labels[0] } return labels[1] } // Substring safely returns the first N characters of a string. func Substring(value string, n int) string { if n > len(value) { return value } return value[:n] } // TrimEllipses is like Substring but will add an ellipses if truncated. func TrimEllipses(value string, n int) string { if n > len(value) { return value } return value[:n] + "…" } // IterRange returns a list of integers useful for pagination. func IterRange(start, n int) []int { var result = []int{} for i := start; i <= n; i++ { result = append(result, i) } return result } // SubtractInt subtracts two numbers. func SubtractInt(a, b int) int { return a - b } // UrlEncode escapes a series of values (joined with no delimiter) func UrlEncode(values ...interface{}) string { var result string for _, value := range values { result += url.QueryEscape(fmt.Sprintf("%v", value)) } return result } // QueryPlus takes the current request's query parameters and upserts them with new values. // // Use it like: {{QueryPlus "page" .NextPage}} // // Returns the query string sans the ? prefix, like "key1=value1&key2=value2" func QueryPlus(r *http.Request) func(...interface{}) template.URL { return func(upsert ...interface{}) template.URL { // Get current parameters. // Note: COPY them from r.Form so we don't accidentally modify r.Form. var params = map[string][]string{} for k, v := range r.Form { params[k] = v } // Mix in the incoming fields. for i := 0; i < len(upsert); i += 2 { var ( key = fmt.Sprintf("%v", upsert[i]) value interface{} ) if len(upsert) > i { value = upsert[i+1] } params[key] = []string{fmt.Sprintf("%v", value)} } // Assemble and return the query string. var parts = []string{} for k, vs := range params { for _, v := range vs { parts = append(parts, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v)), ) } } // Sort them deterministically. sort.Strings(parts) return template.URL(strings.Join(parts, "&")) } } // SimplePager creates a paginator row (partial template). // // Use it like: {{SimplePager .Pager}} // // It runs the template partial 'simple_pager.html' to customize it for the site theme. func SimplePager(r *http.Request) func(*models.Pagination) template.HTML { return func(pager *models.Pagination) template.HTML { tmpl, err := template.New("index").Funcs(template.FuncMap{ "QueryPlus": QueryPlus(r), }).ParseFiles(config.TemplatePath + "/partials/simple_pager.html") if err != nil { return template.HTML(err.Error()) } var ( vars = struct { Pager *models.Pagination Request *http.Request }{pager, r} buf = bytes.NewBuffer([]byte{}) ) err = tmpl.ExecuteTemplate(buf, "SimplePager", vars) if err != nil { return template.HTML(err.Error()) } return template.HTML(buf.String()) } }