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