A vibe coded tangled fork which supports pijul.
at 492f7060ba54516a37428581d23c24b5039d3975 912 lines 23 kB view raw
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/db" 20 "tangled.sh/tangled.sh/core/appview/oauth" 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.Core.Dev, 52 CamoUrl: config.Camo.Host, 53 CamoSecret: config.Camo.SharedSecret, 54 } 55 56 p := &Pages{ 57 t: make(map[string]*template.Template), 58 dev: config.Core.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 252func (p *Pages) OAuthLogin(w io.Writer, params LoginParams) error { 253 return p.executePlain("user/oauthlogin", w, params) 254} 255 256type TimelineParams struct { 257 LoggedInUser *oauth.User 258 Timeline []db.TimelineEvent 259 DidHandleMap map[string]string 260} 261 262func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 263 return p.execute("timeline", w, params) 264} 265 266type SettingsParams struct { 267 LoggedInUser *oauth.User 268 PubKeys []db.PublicKey 269 Emails []db.Email 270} 271 272func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 273 return p.execute("settings", w, params) 274} 275 276type KnotsParams struct { 277 LoggedInUser *oauth.User 278 Registrations []db.Registration 279} 280 281func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 282 return p.execute("knots", w, params) 283} 284 285type KnotParams struct { 286 LoggedInUser *oauth.User 287 DidHandleMap map[string]string 288 Registration *db.Registration 289 Members []string 290 IsOwner bool 291} 292 293func (p *Pages) Knot(w io.Writer, params KnotParams) error { 294 return p.execute("knot", w, params) 295} 296 297type NewRepoParams struct { 298 LoggedInUser *oauth.User 299 Knots []string 300} 301 302func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 303 return p.execute("repo/new", w, params) 304} 305 306type ForkRepoParams struct { 307 LoggedInUser *oauth.User 308 Knots []string 309 RepoInfo repoinfo.RepoInfo 310} 311 312func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 313 return p.execute("repo/fork", w, params) 314} 315 316type ProfilePageParams struct { 317 LoggedInUser *oauth.User 318 Repos []db.Repo 319 CollaboratingRepos []db.Repo 320 ProfileTimeline *db.ProfileTimeline 321 Card ProfileCard 322 323 DidHandleMap map[string]string 324} 325 326type ProfileCard struct { 327 UserDid string 328 UserHandle string 329 FollowStatus db.FollowStatus 330 AvatarUri string 331 Followers int 332 Following int 333 334 Profile *db.Profile 335} 336 337func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 338 return p.execute("user/profile", w, params) 339} 340 341type ReposPageParams struct { 342 LoggedInUser *oauth.User 343 Repos []db.Repo 344 Card ProfileCard 345 346 DidHandleMap map[string]string 347} 348 349func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 350 return p.execute("user/repos", w, params) 351} 352 353type FollowFragmentParams struct { 354 UserDid string 355 FollowStatus db.FollowStatus 356} 357 358func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 359 return p.executePlain("user/fragments/follow", w, params) 360} 361 362type EditBioParams struct { 363 LoggedInUser *oauth.User 364 Profile *db.Profile 365} 366 367func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 368 return p.executePlain("user/fragments/editBio", w, params) 369} 370 371type EditPinsParams struct { 372 LoggedInUser *oauth.User 373 Profile *db.Profile 374 AllRepos []PinnedRepo 375 DidHandleMap map[string]string 376} 377 378type PinnedRepo struct { 379 IsPinned bool 380 db.Repo 381} 382 383func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 384 return p.executePlain("user/fragments/editPins", w, params) 385} 386 387type RepoActionsFragmentParams struct { 388 IsStarred bool 389 RepoAt syntax.ATURI 390 Stats db.RepoStats 391} 392 393func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 394 return p.executePlain("repo/fragments/repoActions", w, params) 395} 396 397type RepoDescriptionParams struct { 398 RepoInfo repoinfo.RepoInfo 399} 400 401func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 402 return p.executePlain("repo/fragments/editRepoDescription", w, params) 403} 404 405func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 406 return p.executePlain("repo/fragments/repoDescription", w, params) 407} 408 409type RepoIndexParams struct { 410 LoggedInUser *oauth.User 411 RepoInfo repoinfo.RepoInfo 412 Active string 413 TagMap map[string][]string 414 CommitsTrunc []*object.Commit 415 TagsTrunc []*types.TagReference 416 BranchesTrunc []types.Branch 417 types.RepoIndexResponse 418 HTMLReadme template.HTML 419 Raw bool 420 EmailToDidOrHandle map[string]string 421} 422 423func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 424 params.Active = "overview" 425 if params.IsEmpty { 426 return p.executeRepo("repo/empty", w, params) 427 } 428 429 p.rctx.RepoInfo = params.RepoInfo 430 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 431 432 if params.ReadmeFileName != "" { 433 var htmlString string 434 ext := filepath.Ext(params.ReadmeFileName) 435 switch ext { 436 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 437 htmlString = p.rctx.RenderMarkdown(params.Readme) 438 params.Raw = false 439 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 440 default: 441 htmlString = string(params.Readme) 442 params.Raw = true 443 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString)) 444 } 445 } 446 447 return p.executeRepo("repo/index", w, params) 448} 449 450type RepoLogParams struct { 451 LoggedInUser *oauth.User 452 RepoInfo repoinfo.RepoInfo 453 TagMap map[string][]string 454 types.RepoLogResponse 455 Active string 456 EmailToDidOrHandle map[string]string 457} 458 459func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 460 params.Active = "overview" 461 return p.executeRepo("repo/log", w, params) 462} 463 464type RepoCommitParams struct { 465 LoggedInUser *oauth.User 466 RepoInfo repoinfo.RepoInfo 467 Active string 468 EmailToDidOrHandle map[string]string 469 470 types.RepoCommitResponse 471} 472 473func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 474 params.Active = "overview" 475 return p.executeRepo("repo/commit", w, params) 476} 477 478type RepoTreeParams struct { 479 LoggedInUser *oauth.User 480 RepoInfo repoinfo.RepoInfo 481 Active string 482 BreadCrumbs [][]string 483 BaseTreeLink string 484 BaseBlobLink string 485 types.RepoTreeResponse 486} 487 488type RepoTreeStats struct { 489 NumFolders uint64 490 NumFiles uint64 491} 492 493func (r RepoTreeParams) TreeStats() RepoTreeStats { 494 numFolders, numFiles := 0, 0 495 for _, f := range r.Files { 496 if !f.IsFile { 497 numFolders += 1 498 } else if f.IsFile { 499 numFiles += 1 500 } 501 } 502 503 return RepoTreeStats{ 504 NumFolders: uint64(numFolders), 505 NumFiles: uint64(numFiles), 506 } 507} 508 509func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 510 params.Active = "overview" 511 return p.execute("repo/tree", w, params) 512} 513 514type RepoBranchesParams struct { 515 LoggedInUser *oauth.User 516 RepoInfo repoinfo.RepoInfo 517 Active string 518 types.RepoBranchesResponse 519} 520 521func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 522 params.Active = "overview" 523 return p.executeRepo("repo/branches", w, params) 524} 525 526type RepoTagsParams struct { 527 LoggedInUser *oauth.User 528 RepoInfo repoinfo.RepoInfo 529 Active string 530 types.RepoTagsResponse 531 ArtifactMap map[plumbing.Hash][]db.Artifact 532 DanglingArtifacts []db.Artifact 533} 534 535func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 536 params.Active = "overview" 537 return p.executeRepo("repo/tags", w, params) 538} 539 540type RepoArtifactParams struct { 541 LoggedInUser *oauth.User 542 RepoInfo repoinfo.RepoInfo 543 Artifact db.Artifact 544} 545 546func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 547 return p.executePlain("repo/fragments/artifact", w, params) 548} 549 550type RepoBlobParams struct { 551 LoggedInUser *oauth.User 552 RepoInfo repoinfo.RepoInfo 553 Active string 554 BreadCrumbs [][]string 555 ShowRendered bool 556 RenderToggle bool 557 RenderedContents template.HTML 558 types.RepoBlobResponse 559} 560 561func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 562 var style *chroma.Style = styles.Get("catpuccin-latte") 563 564 if params.ShowRendered { 565 switch markup.GetFormat(params.Path) { 566 case markup.FormatMarkdown: 567 p.rctx.RepoInfo = params.RepoInfo 568 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 569 params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents))) 570 } 571 } 572 573 if params.Lines < 5000 { 574 c := params.Contents 575 formatter := chromahtml.New( 576 chromahtml.InlineCode(false), 577 chromahtml.WithLineNumbers(true), 578 chromahtml.WithLinkableLineNumbers(true, "L"), 579 chromahtml.Standalone(false), 580 chromahtml.WithClasses(true), 581 ) 582 583 lexer := lexers.Get(filepath.Base(params.Path)) 584 if lexer == nil { 585 lexer = lexers.Fallback 586 } 587 588 iterator, err := lexer.Tokenise(nil, c) 589 if err != nil { 590 return fmt.Errorf("chroma tokenize: %w", err) 591 } 592 593 var code bytes.Buffer 594 err = formatter.Format(&code, style, iterator) 595 if err != nil { 596 return fmt.Errorf("chroma format: %w", err) 597 } 598 599 params.Contents = code.String() 600 } 601 602 params.Active = "overview" 603 return p.executeRepo("repo/blob", w, params) 604} 605 606type Collaborator struct { 607 Did string 608 Handle string 609 Role string 610} 611 612type RepoSettingsParams struct { 613 LoggedInUser *oauth.User 614 RepoInfo repoinfo.RepoInfo 615 Collaborators []Collaborator 616 Active string 617 Branches []string 618 DefaultBranch string 619 // TODO: use repoinfo.roles 620 IsCollaboratorInviteAllowed bool 621} 622 623func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 624 params.Active = "settings" 625 return p.executeRepo("repo/settings", w, params) 626} 627 628type RepoIssuesParams struct { 629 LoggedInUser *oauth.User 630 RepoInfo repoinfo.RepoInfo 631 Active string 632 Issues []db.Issue 633 DidHandleMap map[string]string 634 Page pagination.Page 635 FilteringByOpen bool 636} 637 638func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 639 params.Active = "issues" 640 return p.executeRepo("repo/issues/issues", w, params) 641} 642 643type RepoSingleIssueParams struct { 644 LoggedInUser *oauth.User 645 RepoInfo repoinfo.RepoInfo 646 Active string 647 Issue db.Issue 648 Comments []db.Comment 649 IssueOwnerHandle string 650 DidHandleMap map[string]string 651 652 State string 653} 654 655func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 656 params.Active = "issues" 657 if params.Issue.Open { 658 params.State = "open" 659 } else { 660 params.State = "closed" 661 } 662 return p.execute("repo/issues/issue", w, params) 663} 664 665type RepoNewIssueParams struct { 666 LoggedInUser *oauth.User 667 RepoInfo repoinfo.RepoInfo 668 Active string 669} 670 671func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 672 params.Active = "issues" 673 return p.executeRepo("repo/issues/new", w, params) 674} 675 676type EditIssueCommentParams struct { 677 LoggedInUser *oauth.User 678 RepoInfo repoinfo.RepoInfo 679 Issue *db.Issue 680 Comment *db.Comment 681} 682 683func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 684 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 685} 686 687type SingleIssueCommentParams struct { 688 LoggedInUser *oauth.User 689 DidHandleMap map[string]string 690 RepoInfo repoinfo.RepoInfo 691 Issue *db.Issue 692 Comment *db.Comment 693} 694 695func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 696 return p.executePlain("repo/issues/fragments/issueComment", w, params) 697} 698 699type RepoNewPullParams struct { 700 LoggedInUser *oauth.User 701 RepoInfo repoinfo.RepoInfo 702 Branches []types.Branch 703 Active string 704} 705 706func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 707 params.Active = "pulls" 708 return p.executeRepo("repo/pulls/new", w, params) 709} 710 711type RepoPullsParams struct { 712 LoggedInUser *oauth.User 713 RepoInfo repoinfo.RepoInfo 714 Pulls []*db.Pull 715 Active string 716 DidHandleMap map[string]string 717 FilteringBy db.PullState 718} 719 720func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 721 params.Active = "pulls" 722 return p.executeRepo("repo/pulls/pulls", w, params) 723} 724 725type ResubmitResult uint64 726 727const ( 728 ShouldResubmit ResubmitResult = iota 729 ShouldNotResubmit 730 Unknown 731) 732 733func (r ResubmitResult) Yes() bool { 734 return r == ShouldResubmit 735} 736func (r ResubmitResult) No() bool { 737 return r == ShouldNotResubmit 738} 739func (r ResubmitResult) Unknown() bool { 740 return r == Unknown 741} 742 743type RepoSinglePullParams struct { 744 LoggedInUser *oauth.User 745 RepoInfo repoinfo.RepoInfo 746 Active string 747 DidHandleMap map[string]string 748 Pull *db.Pull 749 MergeCheck types.MergeCheckResponse 750 ResubmitCheck ResubmitResult 751} 752 753func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 754 params.Active = "pulls" 755 return p.executeRepo("repo/pulls/pull", w, params) 756} 757 758type RepoPullPatchParams struct { 759 LoggedInUser *oauth.User 760 DidHandleMap map[string]string 761 RepoInfo repoinfo.RepoInfo 762 Pull *db.Pull 763 Diff *types.NiceDiff 764 Round int 765 Submission *db.PullSubmission 766} 767 768// this name is a mouthful 769func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 770 return p.execute("repo/pulls/patch", w, params) 771} 772 773type RepoPullInterdiffParams struct { 774 LoggedInUser *oauth.User 775 DidHandleMap map[string]string 776 RepoInfo repoinfo.RepoInfo 777 Pull *db.Pull 778 Round int 779 Interdiff *patchutil.InterdiffResult 780} 781 782// this name is a mouthful 783func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 784 return p.execute("repo/pulls/interdiff", w, params) 785} 786 787type PullPatchUploadParams struct { 788 RepoInfo repoinfo.RepoInfo 789} 790 791func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 792 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 793} 794 795type PullCompareBranchesParams struct { 796 RepoInfo repoinfo.RepoInfo 797 Branches []types.Branch 798} 799 800func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 801 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 802} 803 804type PullCompareForkParams struct { 805 RepoInfo repoinfo.RepoInfo 806 Forks []db.Repo 807} 808 809func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 810 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 811} 812 813type PullCompareForkBranchesParams struct { 814 RepoInfo repoinfo.RepoInfo 815 SourceBranches []types.Branch 816 TargetBranches []types.Branch 817} 818 819func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 820 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 821} 822 823type PullResubmitParams struct { 824 LoggedInUser *oauth.User 825 RepoInfo repoinfo.RepoInfo 826 Pull *db.Pull 827 SubmissionId int 828} 829 830func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 831 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 832} 833 834type PullActionsParams struct { 835 LoggedInUser *oauth.User 836 RepoInfo repoinfo.RepoInfo 837 Pull *db.Pull 838 RoundNumber int 839 MergeCheck types.MergeCheckResponse 840 ResubmitCheck ResubmitResult 841} 842 843func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 844 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 845} 846 847type PullNewCommentParams struct { 848 LoggedInUser *oauth.User 849 RepoInfo repoinfo.RepoInfo 850 Pull *db.Pull 851 RoundNumber int 852} 853 854func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 855 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 856} 857 858func (p *Pages) Static() http.Handler { 859 if p.dev { 860 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 861 } 862 863 sub, err := fs.Sub(Files, "static") 864 if err != nil { 865 log.Fatalf("no static dir found? that's crazy: %v", err) 866 } 867 // Custom handler to apply Cache-Control headers for font files 868 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 869} 870 871func Cache(h http.Handler) http.Handler { 872 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 873 path := strings.Split(r.URL.Path, "?")[0] 874 875 if strings.HasSuffix(path, ".css") { 876 // on day for css files 877 w.Header().Set("Cache-Control", "public, max-age=86400") 878 } else { 879 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 880 } 881 h.ServeHTTP(w, r) 882 }) 883} 884 885func CssContentHash() string { 886 cssFile, err := Files.Open("static/tw.css") 887 if err != nil { 888 log.Printf("Error opening CSS file: %v", err) 889 return "" 890 } 891 defer cssFile.Close() 892 893 hasher := sha256.New() 894 if _, err := io.Copy(hasher, cssFile); err != nil { 895 log.Printf("Error hashing CSS file: %v", err) 896 return "" 897 } 898 899 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 900} 901 902func (p *Pages) Error500(w io.Writer) error { 903 return p.execute("errors/500", w, nil) 904} 905 906func (p *Pages) Error404(w io.Writer) error { 907 return p.execute("errors/404", w, nil) 908} 909 910func (p *Pages) Error503(w io.Writer) error { 911 return p.execute("errors/503", w, nil) 912}