hmn/src/templates/templates.go

190 lines
5.1 KiB
Go

package templates
import (
"embed"
"fmt"
"html/template"
"net/url"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"github.com/Masterminds/sprig"
"github.com/teacat/noire"
)
const (
Dayish = time.Hour * 24
Weekish = Dayish * 7
Monthish = Dayish * 30
Yearish = Dayish * 365
)
//go:embed src
var templateFs embed.FS
var Templates map[string]*template.Template
var cachebust string
func Init() {
cachebust = fmt.Sprint(time.Now().Unix())
Templates = make(map[string]*template.Template)
files, _ := templateFs.ReadDir("src")
for _, f := range files {
if strings.HasSuffix(f.Name(), ".html") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs, "src/layouts/*.html", "src/include/*.html", "src/"+f.Name())
if err != nil {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
} else if strings.HasSuffix(f.Name(), ".css") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs, "src/"+f.Name())
if err != nil {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
}
}
}
func names(ts []*template.Template) []string {
result := make([]string, len(ts))
for i, t := range ts {
result[i] = t.Name()
}
return result
}
var HMNTemplateFuncs = template.FuncMap{
"absolutedate": func(t time.Time) string {
return t.Format("January 2, 2006, 3:04pm")
},
"alpha": func(alpha float64, color noire.Color) noire.Color {
color.Alpha = alpha
return color
},
"brighten": func(amount float64, color noire.Color) noire.Color {
return color.Tint(amount)
},
"cachebust": func() string {
return cachebust
},
"color2css": func(color noire.Color) template.CSS {
return template.CSS(color.HTML())
},
"darken": func(amount float64, color noire.Color) noire.Color {
return color.Shade(amount)
},
"hex2color": func(hex string) (noire.Color, error) {
if len(hex) < 6 {
return noire.Color{}, fmt.Errorf("hex color was invalid: %v", hex)
}
return noire.NewHex(hex), nil
},
"lightness": func(lightness float64, color noire.Color) noire.Color {
h, s, _, a := color.HSLA()
return noire.NewHSLA(h, s, lightness*100, a)
},
"projecturl": func(url string, proj interface{}) string {
return hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
},
"projecturlq": func(url string, proj interface{}, query string) string {
absUrl := hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
return fmt.Sprintf("%s?%s", absUrl, query)
},
"query": func(args ...string) string {
query := url.Values{}
for i := 0; i < len(args); i += 2 {
query.Set(args[i], args[i+1])
}
return query.Encode()
},
"relativedate": func(t time.Time) string {
// TODO: Support relative future dates
// NOTE(asaf): Months and years aren't exactly accurate, but good enough for now I guess.
str := func(primary int, primaryName string, secondary int, secondaryName string) string {
result := fmt.Sprintf("%d %s", primary, primaryName)
if primary != 1 {
result += "s"
}
if secondary > 0 {
result += fmt.Sprintf(", %d %s", secondary, secondaryName)
if secondary != 1 {
result += "s"
}
}
return result + " ago"
}
delta := time.Now().Sub(t)
if delta < time.Minute {
return "Less than a minute ago"
} else if delta < time.Hour {
return str(int(delta.Minutes()), "minute", 0, "")
} else if delta < Dayish {
return str(int(delta/time.Hour), "hour", int((delta%time.Hour)/time.Minute), "minute")
} else if delta < Weekish {
return str(int(delta/Dayish), "day", int((delta%Dayish)/time.Hour), "hour")
} else if delta < Monthish {
return str(int(delta/Weekish), "week", int((delta%Weekish)/Dayish), "day")
} else if delta < Yearish {
return str(int(delta/Monthish), "month", int((delta%Monthish)/Weekish), "week")
} else {
return str(int(delta/Yearish), "year", int((delta%Yearish)/Monthish), "month")
}
},
"static": func(filepath string) string {
return hmnurl.BuildPublic(filepath, true)
},
"staticnobust": func(filepath string) string {
return hmnurl.BuildPublic(filepath, false)
},
"statictheme": func(theme string, filepath string) string {
return hmnurl.BuildTheme(filepath, theme, true)
},
"staticthemenobust": func(theme string, filepath string) string {
return hmnurl.BuildTheme(filepath, theme, false)
},
"timehtml": func(formatted string, t time.Time) template.HTML {
iso := t.Format(time.RFC3339)
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
},
}
type ErrInvalidHexColor struct {
color string
}
func (e ErrInvalidHexColor) Error() string {
return fmt.Sprintf("invalid hex color: %s", e.color)
}
func getProjectSubdomain(proj interface{}) string {
subdomain := ""
switch p := proj.(type) {
case Project:
subdomain = p.Subdomain
case int:
// TODO: Look up project from the database
default:
panic(fmt.Errorf("projecturl requires either a templates.Project or a project ID, got %+v", proj))
}
return subdomain
}