forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package pages 2 3import ( 4 "bytes" 5 "crypto/sha256" 6 "embed" 7 "encoding/hex" 8 "fmt" 9 "html/template" 10 "io" 11 "io/fs" 12 "log" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 18 "tangled.sh/tangled.sh/core/appview/auth" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages/markup" 21 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 22 "tangled.sh/tangled.sh/core/appview/pagination" 23 "tangled.sh/tangled.sh/core/patchutil" 24 "tangled.sh/tangled.sh/core/types" 25 26 "github.com/alecthomas/chroma/v2" 27 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 28 "github.com/alecthomas/chroma/v2/lexers" 29 "github.com/alecthomas/chroma/v2/styles" 30 "github.com/bluesky-social/indigo/atproto/syntax" 31 "github.com/go-git/go-git/v5/plumbing/object" 32 "github.com/microcosm-cc/bluemonday" 33) 34 35//go:embed templates/* static 36var Files embed.FS 37 38type Pages struct { 39 t map[string]*template.Template 40 dev bool 41 embedFS embed.FS 42 templateDir string // Path to templates on disk for dev mode 43 rctx *markup.RenderContext 44} 45 46func NewPages(dev bool) *Pages { 47 // initialized with safe defaults, can be overriden per use 48 rctx := &markup.RenderContext{ 49 IsDev: dev, 50 } 51 52 p := &Pages{ 53 t: make(map[string]*template.Template), 54 dev: dev, 55 embedFS: Files, 56 rctx: rctx, 57 templateDir: "appview/pages", 58 } 59 60 // Initial load of all templates 61 p.loadAllTemplates() 62 63 return p 64} 65 66func (p *Pages) loadAllTemplates() { 67 templates := make(map[string]*template.Template) 68 var fragmentPaths []string 69 70 // Use embedded FS for initial loading 71 // First, collect all fragment paths 72 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 73 if err != nil { 74 return err 75 } 76 if d.IsDir() { 77 return nil 78 } 79 if !strings.HasSuffix(path, ".html") { 80 return nil 81 } 82 if !strings.Contains(path, "fragments/") { 83 return nil 84 } 85 name := strings.TrimPrefix(path, "templates/") 86 name = strings.TrimSuffix(name, ".html") 87 tmpl, err := template.New(name). 88 Funcs(funcMap()). 89 ParseFS(p.embedFS, path) 90 if err != nil { 91 log.Fatalf("setting up fragment: %v", err) 92 } 93 templates[name] = tmpl 94 fragmentPaths = append(fragmentPaths, path) 95 log.Printf("loaded fragment: %s", name) 96 return nil 97 }) 98 if err != nil { 99 log.Fatalf("walking template dir for fragments: %v", err) 100 } 101 102 // Then walk through and setup the rest of the templates 103 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 104 if err != nil { 105 return err 106 } 107 if d.IsDir() { 108 return nil 109 } 110 if !strings.HasSuffix(path, "html") { 111 return nil 112 } 113 // Skip fragments as they've already been loaded 114 if strings.Contains(path, "fragments/") { 115 return nil 116 } 117 // Skip layouts 118 if strings.Contains(path, "layouts/") { 119 return nil 120 } 121 name := strings.TrimPrefix(path, "templates/") 122 name = strings.TrimSuffix(name, ".html") 123 // Add the page template on top of the base 124 allPaths := []string{} 125 allPaths = append(allPaths, "templates/layouts/*.html") 126 allPaths = append(allPaths, fragmentPaths...) 127 allPaths = append(allPaths, path) 128 tmpl, err := template.New(name). 129 Funcs(funcMap()). 130 ParseFS(p.embedFS, allPaths...) 131 if err != nil { 132 return fmt.Errorf("setting up template: %w", err) 133 } 134 templates[name] = tmpl 135 log.Printf("loaded template: %s", name) 136 return nil 137 }) 138 if err != nil { 139 log.Fatalf("walking template dir: %v", err) 140 } 141 142 log.Printf("total templates loaded: %d", len(templates)) 143 p.t = templates 144} 145 146// loadTemplateFromDisk loads a template from the filesystem in dev mode 147func (p *Pages) loadTemplateFromDisk(name string) error { 148 if !p.dev { 149 return nil 150 } 151 152 log.Printf("reloading template from disk: %s", name) 153 154 // Find all fragments first 155 var fragmentPaths []string 156 err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 157 if err != nil { 158 return err 159 } 160 if d.IsDir() { 161 return nil 162 } 163 if !strings.HasSuffix(path, ".html") { 164 return nil 165 } 166 if !strings.Contains(path, "fragments/") { 167 return nil 168 } 169 fragmentPaths = append(fragmentPaths, path) 170 return nil 171 }) 172 if err != nil { 173 return fmt.Errorf("walking disk template dir for fragments: %w", err) 174 } 175 176 // Find the template path on disk 177 templatePath := filepath.Join(p.templateDir, "templates", name+".html") 178 if _, err := os.Stat(templatePath); os.IsNotExist(err) { 179 return fmt.Errorf("template not found on disk: %s", name) 180 } 181 182 // Create a new template 183 tmpl := template.New(name).Funcs(funcMap()) 184 185 // Parse layouts 186 layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 187 layouts, err := filepath.Glob(layoutGlob) 188 if err != nil { 189 return fmt.Errorf("finding layout templates: %w", err) 190 } 191 192 // Create paths for parsing 193 allFiles := append(layouts, fragmentPaths...) 194 allFiles = append(allFiles, templatePath) 195 196 // Parse all templates 197 tmpl, err = tmpl.ParseFiles(allFiles...) 198 if err != nil { 199 return fmt.Errorf("parsing template files: %w", err) 200 } 201 202 // Update the template in the map 203 p.t[name] = tmpl 204 log.Printf("template reloaded from disk: %s", name) 205 return nil 206} 207 208func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 209 // In dev mode, reload the template from disk before executing 210 if p.dev { 211 if err := p.loadTemplateFromDisk(templateName); err != nil { 212 log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 213 // Continue with the existing template 214 } 215 } 216 217 tmpl, exists := p.t[templateName] 218 if !exists { 219 return fmt.Errorf("template not found: %s", templateName) 220 } 221 222 if base == "" { 223 return tmpl.Execute(w, params) 224 } else { 225 return tmpl.ExecuteTemplate(w, base, params) 226 } 227} 228 229func (p *Pages) execute(name string, w io.Writer, params any) error { 230 return p.executeOrReload(name, w, "layouts/base", params) 231} 232 233func (p *Pages) executePlain(name string, w io.Writer, params any) error { 234 return p.executeOrReload(name, w, "", params) 235} 236 237func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 238 return p.executeOrReload(name, w, "layouts/repobase", params) 239} 240 241type LoginParams struct { 242} 243 244func (p *Pages) Login(w io.Writer, params LoginParams) error { 245 return p.executePlain("user/login", w, params) 246} 247 248type TimelineParams struct { 249 LoggedInUser *auth.User 250 Timeline []db.TimelineEvent 251 DidHandleMap map[string]string 252} 253 254func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 255 return p.execute("timeline", w, params) 256} 257 258type SettingsParams struct { 259 LoggedInUser *auth.User 260 PubKeys []db.PublicKey 261 Emails []db.Email 262} 263 264func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 265 return p.execute("settings", w, params) 266} 267 268type KnotsParams struct { 269 LoggedInUser *auth.User 270 Registrations []db.Registration 271} 272 273func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 274 return p.execute("knots", w, params) 275} 276 277type KnotParams struct { 278 LoggedInUser *auth.User 279 DidHandleMap map[string]string 280 Registration *db.Registration 281 Members []string 282 IsOwner bool 283} 284 285func (p *Pages) Knot(w io.Writer, params KnotParams) error { 286 return p.execute("knot", w, params) 287} 288 289type NewRepoParams struct { 290 LoggedInUser *auth.User 291 Knots []string 292} 293 294func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 295 return p.execute("repo/new", w, params) 296} 297 298type ForkRepoParams struct { 299 LoggedInUser *auth.User 300 Knots []string 301 RepoInfo repoinfo.RepoInfo 302} 303 304func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 305 return p.execute("repo/fork", w, params) 306} 307 308type ProfilePageParams struct { 309 LoggedInUser *auth.User 310 UserDid string 311 UserHandle string 312 Repos []db.Repo 313 CollaboratingRepos []db.Repo 314 ProfileStats ProfileStats 315 FollowStatus db.FollowStatus 316 AvatarUri string 317 ProfileTimeline *db.ProfileTimeline 318 319 DidHandleMap map[string]string 320} 321 322type ProfileStats struct { 323 Followers int 324 Following int 325} 326 327func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 328 return p.execute("user/profile", w, params) 329} 330 331type FollowFragmentParams struct { 332 UserDid string 333 FollowStatus db.FollowStatus 334} 335 336func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 337 return p.executePlain("user/fragments/follow", w, params) 338} 339 340type RepoActionsFragmentParams struct { 341 IsStarred bool 342 RepoAt syntax.ATURI 343 Stats db.RepoStats 344} 345 346func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 347 return p.executePlain("repo/fragments/repoActions", w, params) 348} 349 350type RepoDescriptionParams struct { 351 RepoInfo repoinfo.RepoInfo 352} 353 354func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 355 return p.executePlain("repo/fragments/editRepoDescription", w, params) 356} 357 358func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 359 return p.executePlain("repo/fragments/repoDescription", w, params) 360} 361 362type RepoIndexParams struct { 363 LoggedInUser *auth.User 364 RepoInfo repoinfo.RepoInfo 365 Active string 366 TagMap map[string][]string 367 CommitsTrunc []*object.Commit 368 TagsTrunc []*types.TagReference 369 BranchesTrunc []types.Branch 370 types.RepoIndexResponse 371 HTMLReadme template.HTML 372 Raw bool 373 EmailToDidOrHandle map[string]string 374} 375 376func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 377 params.Active = "overview" 378 if params.IsEmpty { 379 return p.executeRepo("repo/empty", w, params) 380 } 381 382 p.rctx = &markup.RenderContext{ 383 RepoInfo: params.RepoInfo, 384 IsDev: p.dev, 385 RendererType: markup.RendererTypeRepoMarkdown, 386 } 387 388 if params.ReadmeFileName != "" { 389 var htmlString string 390 ext := filepath.Ext(params.ReadmeFileName) 391 switch ext { 392 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 393 htmlString = p.rctx.RenderMarkdown(params.Readme) 394 params.Raw = false 395 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 396 default: 397 htmlString = string(params.Readme) 398 params.Raw = true 399 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString)) 400 } 401 } 402 403 return p.executeRepo("repo/index", w, params) 404} 405 406type RepoLogParams struct { 407 LoggedInUser *auth.User 408 RepoInfo repoinfo.RepoInfo 409 TagMap map[string][]string 410 types.RepoLogResponse 411 Active string 412 EmailToDidOrHandle map[string]string 413} 414 415func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 416 params.Active = "overview" 417 return p.executeRepo("repo/log", w, params) 418} 419 420type RepoCommitParams struct { 421 LoggedInUser *auth.User 422 RepoInfo repoinfo.RepoInfo 423 Active string 424 EmailToDidOrHandle map[string]string 425 426 types.RepoCommitResponse 427} 428 429func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 430 params.Active = "overview" 431 return p.executeRepo("repo/commit", w, params) 432} 433 434type RepoTreeParams struct { 435 LoggedInUser *auth.User 436 RepoInfo repoinfo.RepoInfo 437 Active string 438 BreadCrumbs [][]string 439 BaseTreeLink string 440 BaseBlobLink string 441 types.RepoTreeResponse 442} 443 444type RepoTreeStats struct { 445 NumFolders uint64 446 NumFiles uint64 447} 448 449func (r RepoTreeParams) TreeStats() RepoTreeStats { 450 numFolders, numFiles := 0, 0 451 for _, f := range r.Files { 452 if !f.IsFile { 453 numFolders += 1 454 } else if f.IsFile { 455 numFiles += 1 456 } 457 } 458 459 return RepoTreeStats{ 460 NumFolders: uint64(numFolders), 461 NumFiles: uint64(numFiles), 462 } 463} 464 465func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 466 params.Active = "overview" 467 return p.execute("repo/tree", w, params) 468} 469 470type RepoBranchesParams struct { 471 LoggedInUser *auth.User 472 RepoInfo repoinfo.RepoInfo 473 Active string 474 types.RepoBranchesResponse 475} 476 477func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 478 params.Active = "overview" 479 return p.executeRepo("repo/branches", w, params) 480} 481 482type RepoTagsParams struct { 483 LoggedInUser *auth.User 484 RepoInfo repoinfo.RepoInfo 485 Active string 486 types.RepoTagsResponse 487} 488 489func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 490 params.Active = "overview" 491 return p.executeRepo("repo/tags", w, params) 492} 493 494type RepoBlobParams struct { 495 LoggedInUser *auth.User 496 RepoInfo repoinfo.RepoInfo 497 Active string 498 BreadCrumbs [][]string 499 ShowRendered bool 500 RenderToggle bool 501 RenderedContents template.HTML 502 types.RepoBlobResponse 503} 504 505func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 506 var style *chroma.Style = styles.Get("catpuccin-latte") 507 508 if params.ShowRendered { 509 switch markup.GetFormat(params.Path) { 510 case markup.FormatMarkdown: 511 p.rctx = &markup.RenderContext{ 512 RepoInfo: params.RepoInfo, 513 IsDev: p.dev, 514 RendererType: markup.RendererTypeRepoMarkdown, 515 } 516 params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents)) 517 } 518 } 519 520 if params.Lines < 5000 { 521 c := params.Contents 522 formatter := chromahtml.New( 523 chromahtml.InlineCode(false), 524 chromahtml.WithLineNumbers(true), 525 chromahtml.WithLinkableLineNumbers(true, "L"), 526 chromahtml.Standalone(false), 527 chromahtml.WithClasses(true), 528 ) 529 530 lexer := lexers.Get(filepath.Base(params.Path)) 531 if lexer == nil { 532 lexer = lexers.Fallback 533 } 534 535 iterator, err := lexer.Tokenise(nil, c) 536 if err != nil { 537 return fmt.Errorf("chroma tokenize: %w", err) 538 } 539 540 var code bytes.Buffer 541 err = formatter.Format(&code, style, iterator) 542 if err != nil { 543 return fmt.Errorf("chroma format: %w", err) 544 } 545 546 params.Contents = code.String() 547 } 548 549 params.Active = "overview" 550 return p.executeRepo("repo/blob", w, params) 551} 552 553type Collaborator struct { 554 Did string 555 Handle string 556 Role string 557} 558 559type RepoSettingsParams struct { 560 LoggedInUser *auth.User 561 RepoInfo repoinfo.RepoInfo 562 Collaborators []Collaborator 563 Active string 564 Branches []string 565 DefaultBranch string 566 // TODO: use repoinfo.roles 567 IsCollaboratorInviteAllowed bool 568} 569 570func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 571 params.Active = "settings" 572 return p.executeRepo("repo/settings", w, params) 573} 574 575type RepoIssuesParams struct { 576 LoggedInUser *auth.User 577 RepoInfo repoinfo.RepoInfo 578 Active string 579 Issues []db.Issue 580 DidHandleMap map[string]string 581 Page pagination.Page 582 FilteringByOpen bool 583} 584 585func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 586 params.Active = "issues" 587 return p.executeRepo("repo/issues/issues", w, params) 588} 589 590type RepoSingleIssueParams struct { 591 LoggedInUser *auth.User 592 RepoInfo repoinfo.RepoInfo 593 Active string 594 Issue db.Issue 595 Comments []db.Comment 596 IssueOwnerHandle string 597 DidHandleMap map[string]string 598 599 State string 600} 601 602func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 603 params.Active = "issues" 604 if params.Issue.Open { 605 params.State = "open" 606 } else { 607 params.State = "closed" 608 } 609 return p.execute("repo/issues/issue", w, params) 610} 611 612type RepoNewIssueParams struct { 613 LoggedInUser *auth.User 614 RepoInfo repoinfo.RepoInfo 615 Active string 616} 617 618func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 619 params.Active = "issues" 620 return p.executeRepo("repo/issues/new", w, params) 621} 622 623type EditIssueCommentParams struct { 624 LoggedInUser *auth.User 625 RepoInfo repoinfo.RepoInfo 626 Issue *db.Issue 627 Comment *db.Comment 628} 629 630func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 631 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 632} 633 634type SingleIssueCommentParams struct { 635 LoggedInUser *auth.User 636 DidHandleMap map[string]string 637 RepoInfo repoinfo.RepoInfo 638 Issue *db.Issue 639 Comment *db.Comment 640} 641 642func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 643 return p.executePlain("repo/issues/fragments/issueComment", w, params) 644} 645 646type RepoNewPullParams struct { 647 LoggedInUser *auth.User 648 RepoInfo repoinfo.RepoInfo 649 Branches []types.Branch 650 Active string 651} 652 653func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 654 params.Active = "pulls" 655 return p.executeRepo("repo/pulls/new", w, params) 656} 657 658type RepoPullsParams struct { 659 LoggedInUser *auth.User 660 RepoInfo repoinfo.RepoInfo 661 Pulls []*db.Pull 662 Active string 663 DidHandleMap map[string]string 664 FilteringBy db.PullState 665} 666 667func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 668 params.Active = "pulls" 669 return p.executeRepo("repo/pulls/pulls", w, params) 670} 671 672type ResubmitResult uint64 673 674const ( 675 ShouldResubmit ResubmitResult = iota 676 ShouldNotResubmit 677 Unknown 678) 679 680func (r ResubmitResult) Yes() bool { 681 return r == ShouldResubmit 682} 683func (r ResubmitResult) No() bool { 684 return r == ShouldNotResubmit 685} 686func (r ResubmitResult) Unknown() bool { 687 return r == Unknown 688} 689 690type RepoSinglePullParams struct { 691 LoggedInUser *auth.User 692 RepoInfo repoinfo.RepoInfo 693 Active string 694 DidHandleMap map[string]string 695 Pull *db.Pull 696 MergeCheck types.MergeCheckResponse 697 ResubmitCheck ResubmitResult 698} 699 700func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 701 params.Active = "pulls" 702 return p.executeRepo("repo/pulls/pull", w, params) 703} 704 705type RepoPullPatchParams struct { 706 LoggedInUser *auth.User 707 DidHandleMap map[string]string 708 RepoInfo repoinfo.RepoInfo 709 Pull *db.Pull 710 Diff *types.NiceDiff 711 Round int 712 Submission *db.PullSubmission 713} 714 715// this name is a mouthful 716func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 717 return p.execute("repo/pulls/patch", w, params) 718} 719 720type RepoPullInterdiffParams struct { 721 LoggedInUser *auth.User 722 DidHandleMap map[string]string 723 RepoInfo repoinfo.RepoInfo 724 Pull *db.Pull 725 Round int 726 Interdiff *patchutil.InterdiffResult 727} 728 729// this name is a mouthful 730func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 731 return p.execute("repo/pulls/interdiff", w, params) 732} 733 734type PullPatchUploadParams struct { 735 RepoInfo repoinfo.RepoInfo 736} 737 738func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 739 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 740} 741 742type PullCompareBranchesParams struct { 743 RepoInfo repoinfo.RepoInfo 744 Branches []types.Branch 745} 746 747func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 748 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 749} 750 751type PullCompareForkParams struct { 752 RepoInfo repoinfo.RepoInfo 753 Forks []db.Repo 754} 755 756func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 757 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 758} 759 760type PullCompareForkBranchesParams struct { 761 RepoInfo repoinfo.RepoInfo 762 SourceBranches []types.Branch 763 TargetBranches []types.Branch 764} 765 766func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 767 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 768} 769 770type PullResubmitParams struct { 771 LoggedInUser *auth.User 772 RepoInfo repoinfo.RepoInfo 773 Pull *db.Pull 774 SubmissionId int 775} 776 777func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 778 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 779} 780 781type PullActionsParams struct { 782 LoggedInUser *auth.User 783 RepoInfo repoinfo.RepoInfo 784 Pull *db.Pull 785 RoundNumber int 786 MergeCheck types.MergeCheckResponse 787 ResubmitCheck ResubmitResult 788} 789 790func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 791 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 792} 793 794type PullNewCommentParams struct { 795 LoggedInUser *auth.User 796 RepoInfo repoinfo.RepoInfo 797 Pull *db.Pull 798 RoundNumber int 799} 800 801func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 802 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 803} 804 805func (p *Pages) Static() http.Handler { 806 if p.dev { 807 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 808 } 809 810 sub, err := fs.Sub(Files, "static") 811 if err != nil { 812 log.Fatalf("no static dir found? that's crazy: %v", err) 813 } 814 // Custom handler to apply Cache-Control headers for font files 815 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 816} 817 818func Cache(h http.Handler) http.Handler { 819 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 820 path := strings.Split(r.URL.Path, "?")[0] 821 822 if strings.HasSuffix(path, ".css") { 823 // on day for css files 824 w.Header().Set("Cache-Control", "public, max-age=86400") 825 } else { 826 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 827 } 828 h.ServeHTTP(w, r) 829 }) 830} 831 832func CssContentHash() string { 833 cssFile, err := Files.Open("static/tw.css") 834 if err != nil { 835 log.Printf("Error opening CSS file: %v", err) 836 return "" 837 } 838 defer cssFile.Close() 839 840 hasher := sha256.New() 841 if _, err := io.Copy(hasher, cssFile); err != nil { 842 log.Printf("Error hashing CSS file: %v", err) 843 return "" 844 } 845 846 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 847} 848 849func (p *Pages) Error500(w io.Writer) error { 850 return p.execute("errors/500", w, nil) 851} 852 853func (p *Pages) Error404(w io.Writer) error { 854 return p.execute("errors/404", w, nil) 855} 856 857func (p *Pages) Error503(w io.Writer) error { 858 return p.execute("errors/503", w, nil) 859}