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