[WIP] music platform user data scraper
teal-fm atproto
at main 3.4 kB view raw
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}