1package pages
2
3// Helpers to load gohtml templates and render them
4// forked and inspired from tangled's implementation
5//https://tangled.org/@tangled.org/core/blob/master/appview/pages/pages.go
6
7import (
8 "embed"
9 "html/template"
10 "io"
11 "io/fs"
12 "net/http"
13 "strings"
14 "time"
15)
16
17//go:embed templates/* static/*
18var Files embed.FS
19
20type Pages struct {
21 cache *TmplCache[string, *template.Template]
22 templateDir string // Path to templates on disk for dev mode
23 embedFS fs.FS
24}
25
26func NewPages() *Pages {
27 return &Pages{
28 cache: NewTmplCache[string, *template.Template](),
29 embedFS: Files,
30 }
31}
32
33func (p *Pages) fragmentPaths() ([]string, error) {
34 var fragmentPaths []string
35 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
36 if err != nil {
37 return err
38 }
39 if d.IsDir() {
40 return nil
41 }
42 if !strings.HasSuffix(path, ".gohtml") {
43 return nil
44 }
45 fragmentPaths = append(fragmentPaths, path)
46 return nil
47 })
48 if err != nil {
49 return nil, err
50 }
51
52 return fragmentPaths, nil
53}
54
55func (p *Pages) pathToName(s string) string {
56 return strings.TrimSuffix(strings.TrimPrefix(s, "templates/"), ".gohtml")
57}
58
59// reverse of pathToName
60func (p *Pages) nameToPath(s string) string {
61 return "templates/" + s + ".gohtml"
62}
63
64// parse without memoization
65func (p *Pages) rawParse(stack ...string) (*template.Template, error) {
66 paths, err := p.fragmentPaths()
67 if err != nil {
68 return nil, err
69 }
70 for _, s := range stack {
71 paths = append(paths, p.nameToPath(s))
72 }
73
74 funcs := p.funcMap()
75 top := stack[len(stack)-1]
76 parsed, err := template.New(top).
77 Funcs(funcs).
78 ParseFS(p.embedFS, paths...)
79 if err != nil {
80 return nil, err
81 }
82
83 return parsed, nil
84}
85
86func (p *Pages) parse(stack ...string) (*template.Template, error) {
87 key := strings.Join(stack, "|")
88
89 if cached, exists := p.cache.Get(key); exists {
90 return cached, nil
91 }
92
93 result, err := p.rawParse(stack...)
94 if err != nil {
95 return nil, err
96 }
97
98 p.cache.Set(key, result)
99 return result, nil
100}
101
102func (p *Pages) funcMap() template.FuncMap {
103 return template.FuncMap{
104 "formatTime": func(t time.Time) string {
105 if t.IsZero() {
106 return "N/A"
107 }
108 return t.Format("Jan 02, 2006 15:04")
109 },
110 }
111}
112
113func (p *Pages) parseBase(top string) (*template.Template, error) {
114 stack := []string{
115 "layouts/base",
116 top,
117 }
118 return p.parse(stack...)
119}
120
121func (p *Pages) Static() http.Handler {
122
123 sub, err := fs.Sub(Files, "static")
124 if err != nil {
125 panic(err)
126 }
127
128 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
129}
130
131func Cache(h http.Handler) http.Handler {
132 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133 path := strings.Split(r.URL.Path, "?")[0]
134 // We may want to change these, just took what tangled has and allows browser side caching
135 if strings.HasSuffix(path, ".css") {
136 // on day for css files
137 w.Header().Set("Cache-Control", "public, max-age=86400")
138 } else {
139 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
140 }
141 h.ServeHTTP(w, r)
142 })
143}
144
145// Execute What loads and renders the HTML page/
146func (p *Pages) Execute(name string, w io.Writer, params any) error {
147 tpl, err := p.parseBase(name)
148 if err != nil {
149 return err
150 }
151
152 return tpl.ExecuteTemplate(w, "layouts/base", params)
153}
154
155// Shared view/template params
156
157type NavBar struct {
158 IsLoggedIn bool
159 LastFMUsername string
160}