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