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