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