this repo has no description
1package pages
2
3import (
4 "embed"
5 "fmt"
6 "html/template"
7 "io"
8 "io/fs"
9 "log"
10 "net/http"
11 "path"
12 "strings"
13
14 "github.com/dustin/go-humanize"
15 "github.com/sotangled/tangled/appview/auth"
16 "github.com/sotangled/tangled/appview/db"
17 "github.com/sotangled/tangled/types"
18)
19
20//go:embed templates/* static/*
21var files embed.FS
22
23type Pages struct {
24 t map[string]*template.Template
25}
26
27func funcMap() template.FuncMap {
28 return template.FuncMap{
29 "split": func(s string) []string {
30 return strings.Split(s, "\n")
31 },
32 "splitOn": func(s, sep string) []string {
33 return strings.Split(s, sep)
34 },
35 "add": func(a, b int) int {
36 return a + b
37 },
38 "sub": func(a, b int) int {
39 return a - b
40 },
41 "cond": func(cond interface{}, a, b string) string {
42 if cond == nil {
43 return b
44 }
45
46 if boolean, ok := cond.(bool); boolean && ok {
47 return a
48 }
49
50 return b
51 },
52 "didOrHandle": func(did, handle string) string {
53 if handle != "" {
54 return fmt.Sprintf("@%s", handle)
55 } else {
56 return did
57 }
58 },
59 "assoc": func(values ...string) ([][]string, error) {
60 if len(values)%2 != 0 {
61 return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
62 }
63 pairs := make([][]string, 0)
64 for i := 0; i < len(values); i += 2 {
65 pairs = append(pairs, []string{values[i], values[i+1]})
66 }
67 return pairs, nil
68 },
69 "append": func(s []string, values ...string) []string {
70 s = append(s, values...)
71 return s
72 },
73 "timeFmt": humanize.Time,
74 "byteFmt": humanize.Bytes,
75 "length": func(v []string) int {
76 return len(v)
77 },
78 "splitN": func(s, sep string, n int) []string {
79 return strings.SplitN(s, sep, n)
80 },
81 "escapeHtml": func(s string) string {
82 return template.HTMLEscapeString(s)
83 },
84 "nl2br": func(text string) template.HTML {
85 return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
86 },
87 "unwrapText": func(text string) string {
88 paragraphs := strings.Split(text, "\n\n")
89
90 for i, p := range paragraphs {
91 lines := strings.Split(p, "\n")
92 paragraphs[i] = strings.Join(lines, " ")
93 }
94
95 return strings.Join(paragraphs, "\n\n")
96 },
97 "sequence": func(n int) []struct{} {
98 return make([]struct{}, n)
99 },
100 }
101}
102
103func NewPages() *Pages {
104 templates := make(map[string]*template.Template)
105
106 // Walk through embedded templates directory and parse all .html files
107 err := fs.WalkDir(files, "templates", func(path string, d fs.DirEntry, err error) error {
108 if err != nil {
109 return err
110 }
111
112 if !d.IsDir() && strings.HasSuffix(path, ".html") {
113 name := strings.TrimPrefix(path, "templates/")
114 name = strings.TrimSuffix(name, ".html")
115
116 if !strings.HasPrefix(path, "templates/layouts/") {
117 // Add the page template on top of the base
118 tmpl, err := template.New(name).
119 Funcs(funcMap()).
120 ParseFS(files, "templates/layouts/*.html", path)
121 if err != nil {
122 return fmt.Errorf("setting up template: %w", err)
123 }
124
125 templates[name] = tmpl
126 log.Printf("loaded template: %s", name)
127 }
128
129 return nil
130 }
131 return nil
132 })
133 if err != nil {
134 log.Fatalf("walking template dir: %v", err)
135 }
136
137 log.Printf("total templates loaded: %d", len(templates))
138
139 return &Pages{
140 t: templates,
141 }
142}
143
144type LoginParams struct {
145}
146
147func (p *Pages) execute(name string, w io.Writer, params any) error {
148 return p.t[name].ExecuteTemplate(w, "layouts/base", params)
149}
150
151func (p *Pages) executePlain(name string, w io.Writer, params any) error {
152 return p.t[name].Execute(w, params)
153}
154
155func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
156 return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
157}
158
159func (p *Pages) Login(w io.Writer, params LoginParams) error {
160 return p.executePlain("user/login", w, params)
161}
162
163type TimelineParams struct {
164 LoggedInUser *auth.User
165}
166
167func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
168 return p.execute("timeline", w, params)
169}
170
171type SettingsParams struct {
172 LoggedInUser *auth.User
173 PubKeys []db.PublicKey
174}
175
176func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
177 return p.execute("settings/keys", w, params)
178}
179
180type KnotsParams struct {
181 LoggedInUser *auth.User
182 Registrations []db.Registration
183}
184
185func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
186 return p.execute("knots", w, params)
187}
188
189type KnotParams struct {
190 LoggedInUser *auth.User
191 Registration *db.Registration
192 Members []string
193 IsOwner bool
194}
195
196func (p *Pages) Knot(w io.Writer, params KnotParams) error {
197 return p.execute("knot", w, params)
198}
199
200type NewRepoParams struct {
201 LoggedInUser *auth.User
202 Knots []string
203}
204
205func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
206 return p.execute("repo/new", w, params)
207}
208
209type ProfilePageParams struct {
210 LoggedInUser *auth.User
211 UserDid string
212 UserHandle string
213 Repos []db.Repo
214 CollaboratingRepos []db.Repo
215}
216
217func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
218 return p.execute("user/profile", w, params)
219}
220
221type RepoInfo struct {
222 Name string
223 OwnerDid string
224 OwnerHandle string
225 Description string
226 SettingsAllowed bool
227}
228
229func (r RepoInfo) OwnerWithAt() string {
230 if r.OwnerHandle != "" {
231 return fmt.Sprintf("@%s", r.OwnerHandle)
232 } else {
233 return r.OwnerDid
234 }
235}
236
237func (r RepoInfo) FullName() string {
238 return path.Join(r.OwnerWithAt(), r.Name)
239}
240
241func (r RepoInfo) GetTabs() [][]string {
242 tabs := [][]string{
243 {"overview", "/"},
244 {"issues", "/issues"},
245 {"pulls", "/pulls"},
246 }
247
248 if r.SettingsAllowed {
249 tabs = append(tabs, []string{"settings", "/settings"})
250 }
251
252 return tabs
253}
254
255type RepoIndexParams struct {
256 LoggedInUser *auth.User
257 RepoInfo RepoInfo
258 Active string
259 types.RepoIndexResponse
260}
261
262func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
263 params.Active = "overview"
264 return p.executeRepo("repo/index", w, params)
265}
266
267type RepoLogParams struct {
268 LoggedInUser *auth.User
269 RepoInfo RepoInfo
270 types.RepoLogResponse
271}
272
273func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
274 return p.execute("repo/log", w, params)
275}
276
277type RepoCommitParams struct {
278 LoggedInUser *auth.User
279 RepoInfo RepoInfo
280 Active string
281 types.RepoCommitResponse
282}
283
284func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
285 params.Active = "overview"
286 return p.executeRepo("repo/commit", w, params)
287}
288
289type RepoTreeParams struct {
290 LoggedInUser *auth.User
291 RepoInfo RepoInfo
292 Active string
293 BreadCrumbs [][]string
294 BaseTreeLink string
295 BaseBlobLink string
296 types.RepoTreeResponse
297}
298
299type RepoTreeStats struct {
300 NumFolders uint64
301 NumFiles uint64
302}
303
304func (r RepoTreeParams) TreeStats() RepoTreeStats {
305 numFolders, numFiles := 0, 0
306 for _, f := range r.Files {
307 if !f.IsFile {
308 numFolders += 1
309 } else if f.IsFile {
310 numFiles += 1
311 }
312 }
313
314 return RepoTreeStats{
315 NumFolders: uint64(numFolders),
316 NumFiles: uint64(numFiles),
317 }
318}
319
320func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
321 params.Active = "overview"
322 return p.execute("repo/tree", w, params)
323}
324
325type RepoBranchesParams struct {
326 LoggedInUser *auth.User
327 RepoInfo RepoInfo
328 types.RepoBranchesResponse
329}
330
331func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
332 return p.executeRepo("repo/branches", w, params)
333}
334
335type RepoTagsParams struct {
336 LoggedInUser *auth.User
337 RepoInfo RepoInfo
338 types.RepoTagsResponse
339}
340
341func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
342 return p.executeRepo("repo/tags", w, params)
343}
344
345type RepoBlobParams struct {
346 LoggedInUser *auth.User
347 RepoInfo RepoInfo
348 Active string
349 BreadCrumbs [][]string
350 types.RepoBlobResponse
351}
352
353func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
354 params.Active = "overview"
355 return p.executeRepo("repo/blob", w, params)
356}
357
358type Collaborator struct {
359 Did string
360 Handle string
361 Role string
362}
363
364type RepoSettingsParams struct {
365 LoggedInUser *auth.User
366 RepoInfo RepoInfo
367 Collaborators []Collaborator
368 Active string
369 IsCollaboratorInviteAllowed bool
370}
371
372func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
373 params.Active = "settings"
374 return p.executeRepo("repo/settings", w, params)
375}
376
377func (p *Pages) Static() http.Handler {
378 sub, err := fs.Sub(files, "static")
379 if err != nil {
380 log.Fatalf("no static dir found? that's crazy: %v", err)
381 }
382 return http.StripPrefix("/static/", http.FileServer(http.FS(sub)))
383}
384
385func (p *Pages) Error500(w io.Writer) error {
386 return p.execute("errors/500", w, nil)
387}
388
389func (p *Pages) Error404(w io.Writer) error {
390 return p.execute("errors/404", w, nil)
391}
392
393func (p *Pages) Error503(w io.Writer) error {
394 return p.execute("errors/503", w, nil)
395}