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