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