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