373 lines
8.5 kB
1
package pages
2
3
import (
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/*
21
var files embed.FS
22
23
type Pages struct {
24
t map[string]*template.Template
25
}
26
27
func 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
"length": func(v []string) int {
75
return len(v)
76
},
77
"splitN": func(s, sep string, n int) []string {
78
return strings.SplitN(s, sep, n)
79
},
80
"escapeHtml": func(s string) string {
81
return template.HTMLEscapeString(s)
82
},
83
"nl2br": func(text string) template.HTML {
84
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
85
},
86
"unwrapText": func(text string) string {
87
paragraphs := strings.Split(text, "\n\n")
88
89
for i, p := range paragraphs {
90
lines := strings.Split(p, "\n")
91
paragraphs[i] = strings.Join(lines, " ")
92
}
93
94
return strings.Join(paragraphs, "\n\n")
95
},
96
"sequence": func(n int) []struct{} {
97
return make([]struct{}, n)
98
},
99
}
100
}
101
102
func NewPages() *Pages {
103
templates := make(map[string]*template.Template)
104
105
// Walk through embedded templates directory and parse all .html files
106
err := fs.WalkDir(files, "templates", func(path string, d fs.DirEntry, err error) error {
107
if err != nil {
108
return err
109
}
110
111
if !d.IsDir() && strings.HasSuffix(path, ".html") {
112
name := strings.TrimPrefix(path, "templates/")
113
name = strings.TrimSuffix(name, ".html")
114
115
if !strings.HasPrefix(path, "templates/layouts/") {
116
// Add the page template on top of the base
117
tmpl, err := template.New(name).
118
Funcs(funcMap()).
119
ParseFS(files, "templates/layouts/*.html", path)
120
if err != nil {
121
return fmt.Errorf("setting up template: %w", err)
122
}
123
124
templates[name] = tmpl
125
log.Printf("loaded template: %s", name)
126
}
127
128
return nil
129
}
130
return nil
131
})
132
if err != nil {
133
log.Fatalf("walking template dir: %v", err)
134
}
135
136
log.Printf("total templates loaded: %d", len(templates))
137
138
return &Pages{
139
t: templates,
140
}
141
}
142
143
type LoginParams struct {
144
}
145
146
func (p *Pages) execute(name string, w io.Writer, params any) error {
147
return p.t[name].ExecuteTemplate(w, "layouts/base", params)
148
}
149
150
func (p *Pages) executePlain(name string, w io.Writer, params any) error {
151
return p.t[name].Execute(w, params)
152
}
153
154
func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
155
return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
156
}
157
158
func (p *Pages) Login(w io.Writer, params LoginParams) error {
159
return p.executePlain("user/login", w, params)
160
}
161
162
type TimelineParams struct {
163
LoggedInUser *auth.User
164
}
165
166
func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
167
return p.execute("timeline", w, params)
168
}
169
170
type SettingsParams struct {
171
LoggedInUser *auth.User
172
PubKeys []db.PublicKey
173
}
174
175
func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
176
return p.execute("settings/keys", w, params)
177
}
178
179
type KnotsParams struct {
180
LoggedInUser *auth.User
181
Registrations []db.Registration
182
}
183
184
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
185
return p.execute("knots", w, params)
186
}
187
188
type KnotParams struct {
189
LoggedInUser *auth.User
190
Registration *db.Registration
191
Members []string
192
IsOwner bool
193
}
194
195
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
196
return p.execute("knot", w, params)
197
}
198
199
type NewRepoParams struct {
200
LoggedInUser *auth.User
201
Knots []string
202
}
203
204
func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
205
return p.execute("repo/new", w, params)
206
}
207
208
type ProfilePageParams struct {
209
LoggedInUser *auth.User
210
UserDid string
211
UserHandle string
212
Repos []db.Repo
213
CollaboratingRepos []db.Repo
214
}
215
216
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
217
return p.execute("user/profile", w, params)
218
}
219
220
type RepoInfo struct {
221
Name string
222
OwnerDid string
223
OwnerHandle string
224
Description string
225
SettingsAllowed bool
226
}
227
228
func (r RepoInfo) OwnerWithAt() string {
229
if r.OwnerHandle != "" {
230
return fmt.Sprintf("@%s", r.OwnerHandle)
231
} else {
232
return r.OwnerDid
233
}
234
}
235
236
func (r RepoInfo) FullName() string {
237
return path.Join(r.OwnerWithAt(), r.Name)
238
}
239
240
func (r RepoInfo) GetTabs() [][]string {
241
tabs := [][]string{
242
{"overview", "/"},
243
{"issues", "/issues"},
244
{"pulls", "/pulls"},
245
}
246
247
if r.SettingsAllowed {
248
tabs = append(tabs, []string{"settings", "/settings"})
249
}
250
251
return tabs
252
}
253
254
type RepoIndexParams struct {
255
LoggedInUser *auth.User
256
RepoInfo RepoInfo
257
Active string
258
types.RepoIndexResponse
259
}
260
261
func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
262
params.Active = "overview"
263
return p.executeRepo("repo/index", w, params)
264
}
265
266
type RepoLogParams struct {
267
LoggedInUser *auth.User
268
RepoInfo RepoInfo
269
types.RepoLogResponse
270
}
271
272
func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
273
return p.execute("repo/log", w, params)
274
}
275
276
type RepoCommitParams struct {
277
LoggedInUser *auth.User
278
RepoInfo RepoInfo
279
Active string
280
types.RepoCommitResponse
281
}
282
283
func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
284
params.Active = "overview"
285
return p.executeRepo("repo/commit", w, params)
286
}
287
288
type RepoTreeParams struct {
289
LoggedInUser *auth.User
290
RepoInfo RepoInfo
291
Active string
292
BreadCrumbs [][]string
293
BaseTreeLink string
294
BaseBlobLink string
295
types.RepoTreeResponse
296
}
297
298
func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
299
params.Active = "overview"
300
return p.execute("repo/tree", w, params)
301
}
302
303
type RepoBranchesParams struct {
304
LoggedInUser *auth.User
305
RepoInfo RepoInfo
306
types.RepoBranchesResponse
307
}
308
309
func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
310
return p.executeRepo("repo/branches", w, params)
311
}
312
313
type RepoTagsParams struct {
314
LoggedInUser *auth.User
315
RepoInfo RepoInfo
316
types.RepoTagsResponse
317
}
318
319
func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
320
return p.executeRepo("repo/tags", w, params)
321
}
322
323
type RepoBlobParams struct {
324
LoggedInUser *auth.User
325
RepoInfo RepoInfo
326
Active string
327
BreadCrumbs [][]string
328
types.RepoBlobResponse
329
}
330
331
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
332
params.Active = "overview"
333
return p.executeRepo("repo/blob", w, params)
334
}
335
336
type Collaborator struct {
337
Did string
338
Handle string
339
Role string
340
}
341
342
type RepoSettingsParams struct {
343
LoggedInUser *auth.User
344
RepoInfo RepoInfo
345
Collaborators []Collaborator
346
Active string
347
IsCollaboratorInviteAllowed bool
348
}
349
350
func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
351
params.Active = "settings"
352
return p.executeRepo("repo/settings", w, params)
353
}
354
355
func (p *Pages) Static() http.Handler {
356
sub, err := fs.Sub(files, "static")
357
if err != nil {
358
log.Fatalf("no static dir found? that's crazy: %v", err)
359
}
360
return http.StripPrefix("/static/", http.FileServer(http.FS(sub)))
361
}
362
363
func (p *Pages) Error500(w io.Writer) error {
364
return p.execute("errors/500", w, nil)
365
}
366
367
func (p *Pages) Error404(w io.Writer) error {
368
return p.execute("errors/404", w, nil)
369
}
370
371
func (p *Pages) Error503(w io.Writer) error {
372
return p.execute("errors/503", w, nil)
373
}
374