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