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