this repo has no description
1package pages
2
3import (
4 "bytes"
5 "embed"
6 "fmt"
7 "html/template"
8 "io"
9 "io/fs"
10 "log"
11 "net/http"
12 "path"
13 "path/filepath"
14 "strings"
15
16 "github.com/alecthomas/chroma/v2"
17 chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
18 "github.com/alecthomas/chroma/v2/lexers"
19 "github.com/alecthomas/chroma/v2/styles"
20 "github.com/sotangled/tangled/appview/auth"
21 "github.com/sotangled/tangled/appview/db"
22 "github.com/sotangled/tangled/types"
23)
24
25//go:embed templates/* static/*
26var files embed.FS
27
28type Pages struct {
29 t map[string]*template.Template
30}
31
32func NewPages() *Pages {
33 templates := make(map[string]*template.Template)
34
35 // Walk through embedded templates directory and parse all .html files
36 err := fs.WalkDir(files, "templates", func(path string, d fs.DirEntry, err error) error {
37 if err != nil {
38 return err
39 }
40
41 if !d.IsDir() && strings.HasSuffix(path, ".html") {
42 name := strings.TrimPrefix(path, "templates/")
43 name = strings.TrimSuffix(name, ".html")
44
45 if !strings.HasPrefix(path, "templates/layouts/") {
46 // Add the page template on top of the base
47 tmpl, err := template.New(name).
48 Funcs(funcMap()).
49 ParseFS(files, "templates/layouts/*.html", path)
50 if err != nil {
51 return fmt.Errorf("setting up template: %w", err)
52 }
53
54 templates[name] = tmpl
55 log.Printf("loaded template: %s", name)
56 }
57
58 return nil
59 }
60 return nil
61 })
62 if err != nil {
63 log.Fatalf("walking template dir: %v", err)
64 }
65
66 log.Printf("total templates loaded: %d", len(templates))
67
68 return &Pages{
69 t: templates,
70 }
71}
72
73type LoginParams struct {
74}
75
76func (p *Pages) execute(name string, w io.Writer, params any) error {
77 return p.t[name].ExecuteTemplate(w, "layouts/base", params)
78}
79
80func (p *Pages) executePlain(name string, w io.Writer, params any) error {
81 return p.t[name].Execute(w, params)
82}
83
84func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
85 return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
86}
87
88func (p *Pages) Login(w io.Writer, params LoginParams) error {
89 return p.executePlain("user/login", w, params)
90}
91
92type TimelineParams struct {
93 LoggedInUser *auth.User
94 Timeline []db.TimelineEvent
95 DidHandleMap map[string]string
96}
97
98func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
99 return p.execute("timeline", w, params)
100}
101
102type SettingsParams struct {
103 LoggedInUser *auth.User
104 PubKeys []db.PublicKey
105}
106
107func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
108 return p.execute("settings/keys", w, params)
109}
110
111type KnotsParams struct {
112 LoggedInUser *auth.User
113 Registrations []db.Registration
114}
115
116func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
117 return p.execute("knots", w, params)
118}
119
120type KnotParams struct {
121 LoggedInUser *auth.User
122 Registration *db.Registration
123 Members []string
124 IsOwner bool
125}
126
127func (p *Pages) Knot(w io.Writer, params KnotParams) error {
128 return p.execute("knot", w, params)
129}
130
131type NewRepoParams struct {
132 LoggedInUser *auth.User
133 Knots []string
134}
135
136func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
137 return p.execute("repo/new", w, params)
138}
139
140type ProfilePageParams struct {
141 LoggedInUser *auth.User
142 UserDid string
143 UserHandle string
144 Repos []db.Repo
145 CollaboratingRepos []db.Repo
146 ProfileStats ProfileStats
147 FollowStatus db.FollowStatus
148 DidHandleMap map[string]string
149 AvatarUri string
150}
151
152type ProfileStats struct {
153 Followers int
154 Following int
155}
156
157func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
158 return p.execute("user/profile", w, params)
159}
160
161type RepoInfo struct {
162 Name string
163 OwnerDid string
164 OwnerHandle string
165 Description string
166 SettingsAllowed bool
167}
168
169func (r RepoInfo) OwnerWithAt() string {
170 if r.OwnerHandle != "" {
171 return fmt.Sprintf("@%s", r.OwnerHandle)
172 } else {
173 return r.OwnerDid
174 }
175}
176
177func (r RepoInfo) FullName() string {
178 return path.Join(r.OwnerWithAt(), r.Name)
179}
180
181func (r RepoInfo) GetTabs() [][]string {
182 tabs := [][]string{
183 {"overview", "/"},
184 {"issues", "/issues"},
185 {"pulls", "/pulls"},
186 }
187
188 if r.SettingsAllowed {
189 tabs = append(tabs, []string{"settings", "/settings"})
190 }
191
192 return tabs
193}
194
195type RepoIndexParams struct {
196 LoggedInUser *auth.User
197 RepoInfo RepoInfo
198 Active string
199 TagMap map[string][]string
200 types.RepoIndexResponse
201}
202
203func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
204 params.Active = "overview"
205 if params.IsEmpty {
206 return p.executeRepo("repo/empty", w, params)
207 }
208 return p.executeRepo("repo/index", w, params)
209}
210
211type RepoLogParams struct {
212 LoggedInUser *auth.User
213 RepoInfo RepoInfo
214 types.RepoLogResponse
215 Active string
216}
217
218func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
219 params.Active = "overview"
220 return p.execute("repo/log", w, params)
221}
222
223type RepoCommitParams struct {
224 LoggedInUser *auth.User
225 RepoInfo RepoInfo
226 Active string
227 types.RepoCommitResponse
228}
229
230func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
231 params.Active = "overview"
232 return p.executeRepo("repo/commit", w, params)
233}
234
235type RepoTreeParams struct {
236 LoggedInUser *auth.User
237 RepoInfo RepoInfo
238 Active string
239 BreadCrumbs [][]string
240 BaseTreeLink string
241 BaseBlobLink string
242 types.RepoTreeResponse
243}
244
245type RepoTreeStats struct {
246 NumFolders uint64
247 NumFiles uint64
248}
249
250func (r RepoTreeParams) TreeStats() RepoTreeStats {
251 numFolders, numFiles := 0, 0
252 for _, f := range r.Files {
253 if !f.IsFile {
254 numFolders += 1
255 } else if f.IsFile {
256 numFiles += 1
257 }
258 }
259
260 return RepoTreeStats{
261 NumFolders: uint64(numFolders),
262 NumFiles: uint64(numFiles),
263 }
264}
265
266func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
267 params.Active = "overview"
268 return p.execute("repo/tree", w, params)
269}
270
271type RepoBranchesParams struct {
272 LoggedInUser *auth.User
273 RepoInfo RepoInfo
274 types.RepoBranchesResponse
275}
276
277func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
278 return p.executeRepo("repo/branches", w, params)
279}
280
281type RepoTagsParams struct {
282 LoggedInUser *auth.User
283 RepoInfo RepoInfo
284 types.RepoTagsResponse
285}
286
287func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
288 return p.executeRepo("repo/tags", w, params)
289}
290
291type RepoBlobParams struct {
292 LoggedInUser *auth.User
293 RepoInfo RepoInfo
294 Active string
295 BreadCrumbs [][]string
296 types.RepoBlobResponse
297}
298
299func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
300 style := styles.Get("bw")
301 b := style.Builder()
302 b.Add(chroma.LiteralString, "noitalic")
303 style, _ = b.Build()
304
305 if params.Lines < 5000 {
306 c := params.Contents
307 formatter := chromahtml.New(
308 chromahtml.InlineCode(true),
309 chromahtml.WithLineNumbers(true),
310 chromahtml.WithLinkableLineNumbers(true, "L"),
311 chromahtml.Standalone(false),
312 )
313
314 lexer := lexers.Get(filepath.Base(params.Path))
315 if lexer == nil {
316 lexer = lexers.Fallback
317 }
318
319 iterator, err := lexer.Tokenise(nil, c)
320 if err != nil {
321 return fmt.Errorf("chroma tokenize: %w", err)
322 }
323
324 var code bytes.Buffer
325 err = formatter.Format(&code, style, iterator)
326 if err != nil {
327 return fmt.Errorf("chroma format: %w", err)
328 }
329
330 params.Contents = code.String()
331 }
332
333 params.Active = "overview"
334 return p.executeRepo("repo/blob", w, params)
335}
336
337type Collaborator struct {
338 Did string
339 Handle string
340 Role string
341}
342
343type RepoSettingsParams struct {
344 LoggedInUser *auth.User
345 RepoInfo RepoInfo
346 Collaborators []Collaborator
347 Active string
348 IsCollaboratorInviteAllowed bool
349}
350
351func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
352 params.Active = "settings"
353 return p.executeRepo("repo/settings", w, params)
354}
355
356type RepoIssuesParams struct {
357 LoggedInUser *auth.User
358 RepoInfo RepoInfo
359 Active string
360 Issues []db.Issue
361 DidHandleMap map[string]string
362}
363
364func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
365 params.Active = "issues"
366 return p.executeRepo("repo/issues/issues", w, params)
367}
368
369type RepoSingleIssueParams struct {
370 LoggedInUser *auth.User
371 RepoInfo RepoInfo
372 Active string
373 Issue db.Issue
374 Comments []db.Comment
375 IssueOwnerHandle string
376 DidHandleMap map[string]string
377
378 State string
379}
380
381func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
382 params.Active = "issues"
383 if params.Issue.Open {
384 params.State = "open"
385 } else {
386 params.State = "closed"
387 }
388 return p.execute("repo/issues/issue", w, params)
389}
390
391type RepoNewIssueParams struct {
392 LoggedInUser *auth.User
393 RepoInfo RepoInfo
394 Active string
395}
396
397func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
398 params.Active = "issues"
399 return p.executeRepo("repo/issues/new", w, params)
400}
401
402type RepoPullsParams struct {
403 LoggedInUser *auth.User
404 RepoInfo RepoInfo
405 Active string
406}
407
408func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
409 params.Active = "pulls"
410 return p.executeRepo("repo/pulls/pulls", w, params)
411}
412
413func (p *Pages) Static() http.Handler {
414 sub, err := fs.Sub(files, "static")
415 if err != nil {
416 log.Fatalf("no static dir found? that's crazy: %v", err)
417 }
418 // Custom handler to apply Cache-Control headers for font files
419 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
420}
421
422func Cache(h http.Handler) http.Handler {
423 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
424 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
425 h.ServeHTTP(w, r)
426 })
427}
428
429func (p *Pages) Error500(w io.Writer) error {
430 return p.execute("errors/500", w, nil)
431}
432
433func (p *Pages) Error404(w io.Writer) error {
434 return p.execute("errors/404", w, nil)
435}
436
437func (p *Pages) Error503(w io.Writer) error {
438 return p.execute("errors/503", w, nil)
439}