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