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