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