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"
)
// 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(),
"ComputeAge": utility.Age,
"Split": strings.Split,
"ToMarkdown": ToMarkdown,
"ToJSON": ToJSON,
"ToHTML": ToHTML,
"PhotoURL": photo.URLPath,
"Now": time.Now,
"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(
`Innercircle`,
)
},
"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),
}
}
// 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]`)
}
}
}
// 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)))
}
}
// 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())
}
}