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", 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 Knot string
168 SettingsAllowed bool
169}
170
171func (r RepoInfo) OwnerWithAt() string {
172 if r.OwnerHandle != "" {
173 return fmt.Sprintf("@%s", r.OwnerHandle)
174 } else {
175 return r.OwnerDid
176 }
177}
178
179func (r RepoInfo) FullName() string {
180 return path.Join(r.OwnerWithAt(), r.Name)
181}
182
183func (r RepoInfo) GetTabs() [][]string {
184 tabs := [][]string{
185 {"overview", "/"},
186 {"issues", "/issues"},
187 {"pulls", "/pulls"},
188 }
189
190 if r.SettingsAllowed {
191 tabs = append(tabs, []string{"settings", "/settings"})
192 }
193
194 return tabs
195}
196
197type RepoIndexParams struct {
198 LoggedInUser *auth.User
199 RepoInfo RepoInfo
200 Active string
201 TagMap map[string][]string
202 types.RepoIndexResponse
203 HTMLReadme template.HTML
204 Raw bool
205}
206
207func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
208 params.Active = "overview"
209 if params.IsEmpty {
210 return p.executeRepo("repo/empty", w, params)
211 }
212
213 if params.ReadmeFileName != "" {
214 var htmlString string
215 ext := filepath.Ext(params.ReadmeFileName)
216 switch ext {
217 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd":
218 htmlString = renderMarkdown(params.Readme)
219 params.Raw = false
220 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString))
221 default:
222 htmlString = string(params.Readme)
223 params.Raw = true
224 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString))
225 }
226 }
227
228 return p.executeRepo("repo/index", w, params)
229}
230
231type RepoLogParams struct {
232 LoggedInUser *auth.User
233 RepoInfo RepoInfo
234 types.RepoLogResponse
235 Active string
236}
237
238func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
239 params.Active = "overview"
240 return p.execute("repo/log", w, params)
241}
242
243type RepoCommitParams struct {
244 LoggedInUser *auth.User
245 RepoInfo RepoInfo
246 Active string
247 types.RepoCommitResponse
248}
249
250func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
251 params.Active = "overview"
252 return p.executeRepo("repo/commit", w, params)
253}
254
255type RepoTreeParams struct {
256 LoggedInUser *auth.User
257 RepoInfo RepoInfo
258 Active string
259 BreadCrumbs [][]string
260 BaseTreeLink string
261 BaseBlobLink string
262 types.RepoTreeResponse
263}
264
265type RepoTreeStats struct {
266 NumFolders uint64
267 NumFiles uint64
268}
269
270func (r RepoTreeParams) TreeStats() RepoTreeStats {
271 numFolders, numFiles := 0, 0
272 for _, f := range r.Files {
273 if !f.IsFile {
274 numFolders += 1
275 } else if f.IsFile {
276 numFiles += 1
277 }
278 }
279
280 return RepoTreeStats{
281 NumFolders: uint64(numFolders),
282 NumFiles: uint64(numFiles),
283 }
284}
285
286func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
287 params.Active = "overview"
288 return p.execute("repo/tree", w, params)
289}
290
291type RepoBranchesParams struct {
292 LoggedInUser *auth.User
293 RepoInfo RepoInfo
294 types.RepoBranchesResponse
295}
296
297func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
298 return p.executeRepo("repo/branches", w, params)
299}
300
301type RepoTagsParams struct {
302 LoggedInUser *auth.User
303 RepoInfo RepoInfo
304 types.RepoTagsResponse
305}
306
307func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
308 return p.executeRepo("repo/tags", w, params)
309}
310
311type RepoBlobParams struct {
312 LoggedInUser *auth.User
313 RepoInfo RepoInfo
314 Active string
315 BreadCrumbs [][]string
316 types.RepoBlobResponse
317}
318
319func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
320 style := styles.Get("bw")
321 b := style.Builder()
322 b.Add(chroma.LiteralString, "noitalic")
323 style, _ = b.Build()
324
325 if params.Lines < 5000 {
326 c := params.Contents
327 formatter := chromahtml.New(
328 chromahtml.InlineCode(true),
329 chromahtml.WithLineNumbers(true),
330 chromahtml.WithLinkableLineNumbers(true, "L"),
331 chromahtml.Standalone(false),
332 )
333
334 lexer := lexers.Get(filepath.Base(params.Path))
335 if lexer == nil {
336 lexer = lexers.Fallback
337 }
338
339 iterator, err := lexer.Tokenise(nil, c)
340 if err != nil {
341 return fmt.Errorf("chroma tokenize: %w", err)
342 }
343
344 var code bytes.Buffer
345 err = formatter.Format(&code, style, iterator)
346 if err != nil {
347 return fmt.Errorf("chroma format: %w", err)
348 }
349
350 params.Contents = code.String()
351 }
352
353 params.Active = "overview"
354 return p.executeRepo("repo/blob", w, params)
355}
356
357type Collaborator struct {
358 Did string
359 Handle string
360 Role string
361}
362
363type RepoSettingsParams struct {
364 LoggedInUser *auth.User
365 RepoInfo RepoInfo
366 Collaborators []Collaborator
367 Active string
368 IsCollaboratorInviteAllowed bool
369}
370
371func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
372 params.Active = "settings"
373 return p.executeRepo("repo/settings", w, params)
374}
375
376type RepoIssuesParams struct {
377 LoggedInUser *auth.User
378 RepoInfo RepoInfo
379 Active string
380 Issues []db.Issue
381 DidHandleMap map[string]string
382}
383
384func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
385 params.Active = "issues"
386 return p.executeRepo("repo/issues/issues", w, params)
387}
388
389type RepoSingleIssueParams struct {
390 LoggedInUser *auth.User
391 RepoInfo RepoInfo
392 Active string
393 Issue db.Issue
394 Comments []db.Comment
395 IssueOwnerHandle string
396 DidHandleMap map[string]string
397
398 State string
399}
400
401func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
402 params.Active = "issues"
403 if params.Issue.Open {
404 params.State = "open"
405 } else {
406 params.State = "closed"
407 }
408 return p.execute("repo/issues/issue", w, params)
409}
410
411type RepoNewIssueParams struct {
412 LoggedInUser *auth.User
413 RepoInfo RepoInfo
414 Active string
415}
416
417func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
418 params.Active = "issues"
419 return p.executeRepo("repo/issues/new", w, params)
420}
421
422type RepoPullsParams struct {
423 LoggedInUser *auth.User
424 RepoInfo RepoInfo
425 Active string
426}
427
428func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
429 params.Active = "pulls"
430 return p.executeRepo("repo/pulls/pulls", w, params)
431}
432
433func (p *Pages) Static() http.Handler {
434 sub, err := fs.Sub(files, "static")
435 if err != nil {
436 log.Fatalf("no static dir found? that's crazy: %v", err)
437 }
438 // Custom handler to apply Cache-Control headers for font files
439 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
440}
441
442func Cache(h http.Handler) http.Handler {
443 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
444 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
445 h.ServeHTTP(w, r)
446 })
447}
448
449func (p *Pages) Error500(w io.Writer) error {
450 return p.execute("errors/500", w, nil)
451}
452
453func (p *Pages) Error404(w io.Writer) error {
454 return p.execute("errors/404", w, nil)
455}
456
457func (p *Pages) Error503(w io.Writer) error {
458 return p.execute("errors/503", w, nil)
459}