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