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 "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
102func 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
143type LoginParams struct {
144}
145
146func (p *Pages) execute(name string, w io.Writer, params any) error {
147 return p.t[name].ExecuteTemplate(w, "layouts/base", params)
148}
149
150func (p *Pages) executePlain(name string, w io.Writer, params any) error {
151 return p.t[name].Execute(w, params)
152}
153
154func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
155 return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
156}
157
158func (p *Pages) Login(w io.Writer, params LoginParams) error {
159 return p.executePlain("user/login", w, params)
160}
161
162type TimelineParams struct {
163 LoggedInUser *auth.User
164}
165
166func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
167 return p.execute("timeline", w, params)
168}
169
170type SettingsParams struct {
171 LoggedInUser *auth.User
172 PubKeys []db.PublicKey
173}
174
175func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
176 return p.execute("settings/keys", w, params)
177}
178
179type KnotsParams struct {
180 LoggedInUser *auth.User
181 Registrations []db.Registration
182}
183
184func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
185 return p.execute("knots", w, params)
186}
187
188type KnotParams struct {
189 LoggedInUser *auth.User
190 Registration *db.Registration
191 Members []string
192 IsOwner bool
193}
194
195func (p *Pages) Knot(w io.Writer, params KnotParams) error {
196 return p.execute("knot", w, params)
197}
198
199type NewRepoParams struct {
200 LoggedInUser *auth.User
201 Knots []string
202}
203
204func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
205 return p.execute("repo/new", w, params)
206}
207
208type ProfilePageParams struct {
209 LoggedInUser *auth.User
210 UserDid string
211 UserHandle string
212 Repos []db.Repo
213 CollaboratingRepos []db.Repo
214}
215
216func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
217 return p.execute("user/profile", w, params)
218}
219
220type RepoInfo struct {
221 Name string
222 OwnerDid string
223 OwnerHandle string
224 Description string
225 SettingsAllowed bool
226}
227
228func (r RepoInfo) OwnerWithAt() string {
229 if r.OwnerHandle != "" {
230 return fmt.Sprintf("@%s", r.OwnerHandle)
231 } else {
232 return r.OwnerDid
233 }
234}
235
236func (r RepoInfo) FullName() string {
237 return path.Join(r.OwnerWithAt(), r.Name)
238}
239
240func (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
254type RepoIndexParams struct {
255 LoggedInUser *auth.User
256 RepoInfo RepoInfo
257 Active string
258 types.RepoIndexResponse
259}
260
261func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
262 params.Active = "overview"
263 return p.executeRepo("repo/index", w, params)
264}
265
266type RepoLogParams struct {
267 LoggedInUser *auth.User
268 RepoInfo RepoInfo
269 types.RepoLogResponse
270}
271
272func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
273 return p.execute("repo/log", w, params)
274}
275
276type RepoCommitParams struct {
277 LoggedInUser *auth.User
278 RepoInfo RepoInfo
279 Active string
280 types.RepoCommitResponse
281}
282
283func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
284 params.Active = "overview"
285 return p.executeRepo("repo/commit", w, params)
286}
287
288type 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
298func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
299 params.Active = "overview"
300 return p.execute("repo/tree", w, params)
301}
302
303type RepoBranchesParams struct {
304 LoggedInUser *auth.User
305 RepoInfo RepoInfo
306 types.RepoBranchesResponse
307}
308
309func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
310 return p.executeRepo("repo/branches", w, params)
311}
312
313type RepoTagsParams struct {
314 LoggedInUser *auth.User
315 RepoInfo RepoInfo
316 types.RepoTagsResponse
317}
318
319func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
320 return p.executeRepo("repo/tags", w, params)
321}
322
323type RepoBlobParams struct {
324 LoggedInUser *auth.User
325 RepoInfo RepoInfo
326 Active string
327 BreadCrumbs [][]string
328 types.RepoBlobResponse
329}
330
331func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
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
355func (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
363func (p *Pages) Error500(w io.Writer) error {
364 return p.execute("errors/500", w, nil)
365}
366
367func (p *Pages) Error404(w io.Writer) error {
368 return p.execute("errors/404", w, nil)
369}
370
371func (p *Pages) Error503(w io.Writer) error {
372 return p.execute("errors/503", w, nil)
373}