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/slog" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 "sync" 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview/commitverify" 21 "tangled.sh/tangled.sh/core/appview/config" 22 "tangled.sh/tangled.sh/core/appview/db" 23 "tangled.sh/tangled.sh/core/appview/oauth" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 "tangled.sh/tangled.sh/core/idresolver" 28 "tangled.sh/tangled.sh/core/patchutil" 29 "tangled.sh/tangled.sh/core/types" 30 31 "github.com/alecthomas/chroma/v2" 32 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 33 "github.com/alecthomas/chroma/v2/lexers" 34 "github.com/alecthomas/chroma/v2/styles" 35 "github.com/bluesky-social/indigo/atproto/identity" 36 "github.com/bluesky-social/indigo/atproto/syntax" 37 "github.com/go-git/go-git/v5/plumbing" 38 "github.com/go-git/go-git/v5/plumbing/object" 39) 40 41//go:embed templates/* static 42var Files embed.FS 43 44type Pages struct { 45 mu sync.RWMutex 46 cache *TmplCache[string, *template.Template] 47 48 avatar config.AvatarConfig 49 resolver *idresolver.Resolver 50 dev bool 51 embedFS fs.FS 52 templateDir string // Path to templates on disk for dev mode 53 rctx *markup.RenderContext 54 logger *slog.Logger 55} 56 57func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { 58 // initialized with safe defaults, can be overriden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, 61 CamoUrl: config.Camo.Host, 62 CamoSecret: config.Camo.SharedSecret, 63 Sanitizer: markup.NewSanitizer(), 64 } 65 66 p := &Pages{ 67 mu: sync.RWMutex{}, 68 cache: NewTmplCache[string, *template.Template](), 69 dev: config.Core.Dev, 70 avatar: config.Avatar, 71 rctx: rctx, 72 resolver: res, 73 templateDir: "appview/pages", 74 logger: slog.Default().With("component", "pages"), 75 } 76 77 if p.dev { 78 p.embedFS = os.DirFS(p.templateDir) 79 } else { 80 p.embedFS = Files 81 } 82 83 return p 84} 85 86func (p *Pages) pathToName(s string) string { 87 return strings.TrimSuffix(strings.TrimPrefix(s, "templates/"), ".html") 88} 89 90// reverse of pathToName 91func (p *Pages) nameToPath(s string) string { 92 return "templates/" + s + ".html" 93} 94 95func (p *Pages) fragmentPaths() ([]string, error) { 96 var fragmentPaths []string 97 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 98 if err != nil { 99 return err 100 } 101 if d.IsDir() { 102 return nil 103 } 104 if !strings.HasSuffix(path, ".html") { 105 return nil 106 } 107 if !strings.Contains(path, "fragments/") { 108 return nil 109 } 110 fragmentPaths = append(fragmentPaths, path) 111 return nil 112 }) 113 if err != nil { 114 return nil, err 115 } 116 117 return fragmentPaths, nil 118} 119 120func (p *Pages) fragments() (*template.Template, error) { 121 fragmentPaths, err := p.fragmentPaths() 122 if err != nil { 123 return nil, err 124 } 125 126 funcs := p.funcMap() 127 128 // parse all fragments together 129 allFragments := template.New("").Funcs(funcs) 130 for _, f := range fragmentPaths { 131 name := p.pathToName(f) 132 133 pf, err := template.New(name). 134 Funcs(funcs). 135 ParseFS(p.embedFS, f) 136 if err != nil { 137 return nil, err 138 } 139 140 allFragments, err = allFragments.AddParseTree(name, pf.Tree) 141 if err != nil { 142 return nil, err 143 } 144 } 145 146 return allFragments, nil 147} 148 149// parse without memoization 150func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 151 paths, err := p.fragmentPaths() 152 if err != nil { 153 return nil, err 154 } 155 for _, s := range stack { 156 paths = append(paths, p.nameToPath(s)) 157 } 158 159 funcs := p.funcMap() 160 top := stack[len(stack)-1] 161 parsed, err := template.New(top). 162 Funcs(funcs). 163 ParseFS(p.embedFS, paths...) 164 if err != nil { 165 return nil, err 166 } 167 168 return parsed, nil 169} 170 171func (p *Pages) parse(stack ...string) (*template.Template, error) { 172 key := strings.Join(stack, "|") 173 174 // never cache in dev mode 175 if cached, exists := p.cache.Get(key); !p.dev && exists { 176 return cached, nil 177 } 178 179 result, err := p.rawParse(stack...) 180 if err != nil { 181 return nil, err 182 } 183 184 p.cache.Set(key, result) 185 return result, nil 186} 187 188func (p *Pages) parseBase(top string) (*template.Template, error) { 189 stack := []string{ 190 "layouts/base", 191 top, 192 } 193 return p.parse(stack...) 194} 195 196func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 197 stack := []string{ 198 "layouts/base", 199 "layouts/repobase", 200 top, 201 } 202 return p.parse(stack...) 203} 204 205func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 206 stack := []string{ 207 "layouts/base", 208 "layouts/profilebase", 209 top, 210 } 211 return p.parse(stack...) 212} 213 214func (p *Pages) executePlain(name string, w io.Writer, params any) error { 215 tpl, err := p.parse(name) 216 if err != nil { 217 return err 218 } 219 220 return tpl.Execute(w, params) 221} 222 223func (p *Pages) execute(name string, w io.Writer, params any) error { 224 tpl, err := p.parseBase(name) 225 if err != nil { 226 return err 227 } 228 229 return tpl.ExecuteTemplate(w, "layouts/base", params) 230} 231 232func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 233 tpl, err := p.parseRepoBase(name) 234 if err != nil { 235 return err 236 } 237 238 return tpl.ExecuteTemplate(w, "layouts/base", params) 239} 240 241func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 242 tpl, err := p.parseProfileBase(name) 243 if err != nil { 244 return err 245 } 246 247 return tpl.ExecuteTemplate(w, "layouts/base", params) 248} 249 250func (p *Pages) Favicon(w io.Writer) error { 251 return p.executePlain("favicon", w, nil) 252} 253 254type LoginParams struct { 255 ReturnUrl string 256} 257 258func (p *Pages) Login(w io.Writer, params LoginParams) error { 259 return p.executePlain("user/login", w, params) 260} 261 262func (p *Pages) Signup(w io.Writer) error { 263 return p.executePlain("user/signup", w, nil) 264} 265 266func (p *Pages) CompleteSignup(w io.Writer) error { 267 return p.executePlain("user/completeSignup", w, nil) 268} 269 270type TermsOfServiceParams struct { 271 LoggedInUser *oauth.User 272} 273 274func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 275 return p.execute("legal/terms", w, params) 276} 277 278type PrivacyPolicyParams struct { 279 LoggedInUser *oauth.User 280} 281 282func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 283 return p.execute("legal/privacy", w, params) 284} 285 286type TimelineParams struct { 287 LoggedInUser *oauth.User 288 Timeline []db.TimelineEvent 289 Repos []db.Repo 290} 291 292func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 293 return p.execute("timeline/timeline", w, params) 294} 295 296type UserProfileSettingsParams struct { 297 LoggedInUser *oauth.User 298 Tabs []map[string]any 299 Tab string 300} 301 302func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 303 return p.execute("user/settings/profile", w, params) 304} 305 306type UserKeysSettingsParams struct { 307 LoggedInUser *oauth.User 308 PubKeys []db.PublicKey 309 Tabs []map[string]any 310 Tab string 311} 312 313func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 314 return p.execute("user/settings/keys", w, params) 315} 316 317type UserEmailsSettingsParams struct { 318 LoggedInUser *oauth.User 319 Emails []db.Email 320 Tabs []map[string]any 321 Tab string 322} 323 324func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 325 return p.execute("user/settings/emails", w, params) 326} 327 328type KnotBannerParams struct { 329 Registrations []db.Registration 330} 331 332func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { 333 return p.executePlain("knots/fragments/banner", w, params) 334} 335 336type KnotsParams struct { 337 LoggedInUser *oauth.User 338 Registrations []db.Registration 339} 340 341func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 342 return p.execute("knots/index", w, params) 343} 344 345type KnotParams struct { 346 LoggedInUser *oauth.User 347 Registration *db.Registration 348 Members []string 349 Repos map[string][]db.Repo 350 IsOwner bool 351} 352 353func (p *Pages) Knot(w io.Writer, params KnotParams) error { 354 return p.execute("knots/dashboard", w, params) 355} 356 357type KnotListingParams struct { 358 *db.Registration 359} 360 361func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 362 return p.executePlain("knots/fragments/knotListing", w, params) 363} 364 365type SpindlesParams struct { 366 LoggedInUser *oauth.User 367 Spindles []db.Spindle 368} 369 370func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 371 return p.execute("spindles/index", w, params) 372} 373 374type SpindleListingParams struct { 375 db.Spindle 376} 377 378func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 379 return p.executePlain("spindles/fragments/spindleListing", w, params) 380} 381 382type SpindleDashboardParams struct { 383 LoggedInUser *oauth.User 384 Spindle db.Spindle 385 Members []string 386 Repos map[string][]db.Repo 387} 388 389func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 390 return p.execute("spindles/dashboard", w, params) 391} 392 393type NewRepoParams struct { 394 LoggedInUser *oauth.User 395 Knots []string 396} 397 398func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 399 return p.execute("repo/new", w, params) 400} 401 402type ForkRepoParams struct { 403 LoggedInUser *oauth.User 404 Knots []string 405 RepoInfo repoinfo.RepoInfo 406} 407 408func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 409 return p.execute("repo/fork", w, params) 410} 411 412type ProfileCard struct { 413 UserDid string 414 UserHandle string 415 FollowStatus db.FollowStatus 416 FollowersCount int 417 FollowingCount int 418 Punchcard *db.Punchcard 419 Profile *db.Profile 420 Active string 421} 422 423func (p *ProfileCard) GetTabs() [][]string { 424 tabs := [][]string{ 425 {"overview", "overview", "square-chart-gantt"}, 426 {"repos", "repos", "book-marked"}, 427 {"starred", "starred", "star"}, 428 } 429 430 return tabs 431} 432 433type ProfileOverviewParams struct { 434 LoggedInUser *oauth.User 435 Repos []db.Repo 436 CollaboratingRepos []db.Repo 437 ProfileTimeline *db.ProfileTimeline 438 Card *ProfileCard 439 Active string 440} 441 442func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 443 params.Active = "overview" 444 return p.executeProfile("user/overview", w, params) 445} 446 447 Profile *db.Profile 448} 449 450func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 451 return p.execute("user/profile", w, params) 452} 453 454type ReposPageParams struct { 455 LoggedInUser *oauth.User 456 Repos []db.Repo 457 Card ProfileCard 458} 459 460func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 461 return p.execute("user/repos", w, params) 462} 463 464type FollowCard struct { 465 UserDid string 466 FollowStatus db.FollowStatus 467 FollowersCount int 468 FollowingCount int 469 Profile *db.Profile 470} 471 472type FollowersPageParams struct { 473 LoggedInUser *oauth.User 474 Followers []FollowCard 475 Card ProfileCard 476} 477 478func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 479 return p.execute("user/followers", w, params) 480} 481 482type FollowingPageParams struct { 483 LoggedInUser *oauth.User 484 Following []FollowCard 485 Card ProfileCard 486} 487 488func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 489 return p.execute("user/following", w, params) 490} 491 492type FollowFragmentParams struct { 493 UserDid string 494 FollowStatus db.FollowStatus 495} 496 497func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 498 return p.executePlain("user/fragments/follow", w, params) 499} 500 501type EditBioParams struct { 502 LoggedInUser *oauth.User 503 Profile *db.Profile 504} 505 506func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 507 return p.executePlain("user/fragments/editBio", w, params) 508} 509 510type EditPinsParams struct { 511 LoggedInUser *oauth.User 512 Profile *db.Profile 513 AllRepos []PinnedRepo 514} 515 516type PinnedRepo struct { 517 IsPinned bool 518 db.Repo 519} 520 521func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 522 return p.executePlain("user/fragments/editPins", w, params) 523} 524 525type RepoStarFragmentParams struct { 526 IsStarred bool 527 RepoAt syntax.ATURI 528 Stats db.RepoStats 529} 530 531func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error { 532 return p.executePlain("repo/fragments/repoStar", w, params) 533} 534 535type RepoDescriptionParams struct { 536 RepoInfo repoinfo.RepoInfo 537} 538 539func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 540 return p.executePlain("repo/fragments/editRepoDescription", w, params) 541} 542 543func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 544 return p.executePlain("repo/fragments/repoDescription", w, params) 545} 546 547type RepoIndexParams struct { 548 LoggedInUser *oauth.User 549 RepoInfo repoinfo.RepoInfo 550 Active string 551 TagMap map[string][]string 552 CommitsTrunc []*object.Commit 553 TagsTrunc []*types.TagReference 554 BranchesTrunc []types.Branch 555 // ForkInfo *types.ForkInfo 556 HTMLReadme template.HTML 557 Raw bool 558 EmailToDidOrHandle map[string]string 559 VerifiedCommits commitverify.VerifiedCommits 560 Languages []types.RepoLanguageDetails 561 Pipelines map[string]db.Pipeline 562 types.RepoIndexResponse 563} 564 565func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 566 params.Active = "overview" 567 if params.IsEmpty { 568 return p.executeRepo("repo/empty", w, params) 569 } 570 571 p.rctx.RepoInfo = params.RepoInfo 572 p.rctx.RepoInfo.Ref = params.Ref 573 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 574 575 if params.ReadmeFileName != "" { 576 ext := filepath.Ext(params.ReadmeFileName) 577 switch ext { 578 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 579 params.Raw = false 580 htmlString := p.rctx.RenderMarkdown(params.Readme) 581 sanitized := p.rctx.SanitizeDefault(htmlString) 582 params.HTMLReadme = template.HTML(sanitized) 583 default: 584 params.Raw = true 585 } 586 } 587 588 return p.executeRepo("repo/index", w, params) 589} 590 591type RepoLogParams struct { 592 LoggedInUser *oauth.User 593 RepoInfo repoinfo.RepoInfo 594 TagMap map[string][]string 595 types.RepoLogResponse 596 Active string 597 EmailToDidOrHandle map[string]string 598 VerifiedCommits commitverify.VerifiedCommits 599 Pipelines map[string]db.Pipeline 600} 601 602func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 603 params.Active = "overview" 604 return p.executeRepo("repo/log", w, params) 605} 606 607type RepoCommitParams struct { 608 LoggedInUser *oauth.User 609 RepoInfo repoinfo.RepoInfo 610 Active string 611 EmailToDidOrHandle map[string]string 612 Pipeline *db.Pipeline 613 DiffOpts types.DiffOpts 614 615 // singular because it's always going to be just one 616 VerifiedCommit commitverify.VerifiedCommits 617 618 types.RepoCommitResponse 619} 620 621func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 622 params.Active = "overview" 623 return p.executeRepo("repo/commit", w, params) 624} 625 626type RepoTreeParams struct { 627 LoggedInUser *oauth.User 628 RepoInfo repoinfo.RepoInfo 629 Active string 630 BreadCrumbs [][]string 631 TreePath string 632 types.RepoTreeResponse 633} 634 635type RepoTreeStats struct { 636 NumFolders uint64 637 NumFiles uint64 638} 639 640func (r RepoTreeParams) TreeStats() RepoTreeStats { 641 numFolders, numFiles := 0, 0 642 for _, f := range r.Files { 643 if !f.IsFile { 644 numFolders += 1 645 } else if f.IsFile { 646 numFiles += 1 647 } 648 } 649 650 return RepoTreeStats{ 651 NumFolders: uint64(numFolders), 652 NumFiles: uint64(numFiles), 653 } 654} 655 656func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 657 params.Active = "overview" 658 return p.executeRepo("repo/tree", w, params) 659} 660 661type RepoBranchesParams struct { 662 LoggedInUser *oauth.User 663 RepoInfo repoinfo.RepoInfo 664 Active string 665 types.RepoBranchesResponse 666} 667 668func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 669 params.Active = "overview" 670 return p.executeRepo("repo/branches", w, params) 671} 672 673type RepoTagsParams struct { 674 LoggedInUser *oauth.User 675 RepoInfo repoinfo.RepoInfo 676 Active string 677 types.RepoTagsResponse 678 ArtifactMap map[plumbing.Hash][]db.Artifact 679 DanglingArtifacts []db.Artifact 680} 681 682func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 683 params.Active = "overview" 684 return p.executeRepo("repo/tags", w, params) 685} 686 687type RepoArtifactParams struct { 688 LoggedInUser *oauth.User 689 RepoInfo repoinfo.RepoInfo 690 Artifact db.Artifact 691} 692 693func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 694 return p.executePlain("repo/fragments/artifact", w, params) 695} 696 697type RepoBlobParams struct { 698 LoggedInUser *oauth.User 699 RepoInfo repoinfo.RepoInfo 700 Active string 701 Unsupported bool 702 IsImage bool 703 IsVideo bool 704 ContentSrc string 705 BreadCrumbs [][]string 706 ShowRendered bool 707 RenderToggle bool 708 RenderedContents template.HTML 709 types.RepoBlobResponse 710} 711 712func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 713 var style *chroma.Style = styles.Get("catpuccin-latte") 714 715 if params.ShowRendered { 716 switch markup.GetFormat(params.Path) { 717 case markup.FormatMarkdown: 718 p.rctx.RepoInfo = params.RepoInfo 719 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 720 htmlString := p.rctx.RenderMarkdown(params.Contents) 721 sanitized := p.rctx.SanitizeDefault(htmlString) 722 params.RenderedContents = template.HTML(sanitized) 723 } 724 } 725 726 c := params.Contents 727 formatter := chromahtml.New( 728 chromahtml.InlineCode(false), 729 chromahtml.WithLineNumbers(true), 730 chromahtml.WithLinkableLineNumbers(true, "L"), 731 chromahtml.Standalone(false), 732 chromahtml.WithClasses(true), 733 ) 734 735 lexer := lexers.Get(filepath.Base(params.Path)) 736 if lexer == nil { 737 lexer = lexers.Fallback 738 } 739 740 iterator, err := lexer.Tokenise(nil, c) 741 if err != nil { 742 return fmt.Errorf("chroma tokenize: %w", err) 743 } 744 745 var code bytes.Buffer 746 err = formatter.Format(&code, style, iterator) 747 if err != nil { 748 return fmt.Errorf("chroma format: %w", err) 749 } 750 751 params.Contents = code.String() 752 params.Active = "overview" 753 return p.executeRepo("repo/blob", w, params) 754} 755 756type Collaborator struct { 757 Did string 758 Handle string 759 Role string 760} 761 762type RepoSettingsParams struct { 763 LoggedInUser *oauth.User 764 RepoInfo repoinfo.RepoInfo 765 Collaborators []Collaborator 766 Active string 767 Branches []types.Branch 768 Spindles []string 769 CurrentSpindle string 770 Secrets []*tangled.RepoListSecrets_Secret 771 772 // TODO: use repoinfo.roles 773 IsCollaboratorInviteAllowed bool 774} 775 776func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 777 params.Active = "settings" 778 return p.executeRepo("repo/settings", w, params) 779} 780 781type RepoGeneralSettingsParams struct { 782 LoggedInUser *oauth.User 783 RepoInfo repoinfo.RepoInfo 784 Active string 785 Tabs []map[string]any 786 Tab string 787 Branches []types.Branch 788} 789 790func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 791 params.Active = "settings" 792 return p.executeRepo("repo/settings/general", w, params) 793} 794 795type RepoAccessSettingsParams struct { 796 LoggedInUser *oauth.User 797 RepoInfo repoinfo.RepoInfo 798 Active string 799 Tabs []map[string]any 800 Tab string 801 Collaborators []Collaborator 802} 803 804func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 805 params.Active = "settings" 806 return p.executeRepo("repo/settings/access", w, params) 807} 808 809type RepoPipelineSettingsParams struct { 810 LoggedInUser *oauth.User 811 RepoInfo repoinfo.RepoInfo 812 Active string 813 Tabs []map[string]any 814 Tab string 815 Spindles []string 816 CurrentSpindle string 817 Secrets []map[string]any 818} 819 820func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 821 params.Active = "settings" 822 return p.executeRepo("repo/settings/pipelines", w, params) 823} 824 825type RepoIssuesParams struct { 826 LoggedInUser *oauth.User 827 RepoInfo repoinfo.RepoInfo 828 Active string 829 Issues []db.Issue 830 Page pagination.Page 831 FilteringByOpen bool 832} 833 834func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 835 params.Active = "issues" 836 return p.executeRepo("repo/issues/issues", w, params) 837} 838 839type RepoSingleIssueParams struct { 840 LoggedInUser *oauth.User 841 RepoInfo repoinfo.RepoInfo 842 Active string 843 Issue *db.Issue 844 Comments []db.Comment 845 IssueOwnerHandle string 846 847 OrderedReactionKinds []db.ReactionKind 848 Reactions map[db.ReactionKind]int 849 UserReacted map[db.ReactionKind]bool 850 851 State string 852} 853 854type ThreadReactionFragmentParams struct { 855 ThreadAt syntax.ATURI 856 Kind db.ReactionKind 857 Count int 858 IsReacted bool 859} 860 861func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 862 return p.executePlain("repo/fragments/reaction", w, params) 863} 864 865func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 866 params.Active = "issues" 867 if params.Issue.Open { 868 params.State = "open" 869 } else { 870 params.State = "closed" 871 } 872 return p.executeRepo("repo/issues/issue", w, params) 873} 874 875type RepoNewIssueParams struct { 876 LoggedInUser *oauth.User 877 RepoInfo repoinfo.RepoInfo 878 Active string 879} 880 881func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 882 params.Active = "issues" 883 return p.executeRepo("repo/issues/new", w, params) 884} 885 886type EditIssueCommentParams struct { 887 LoggedInUser *oauth.User 888 RepoInfo repoinfo.RepoInfo 889 Issue *db.Issue 890 Comment *db.Comment 891} 892 893func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 894 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 895} 896 897type SingleIssueCommentParams struct { 898 LoggedInUser *oauth.User 899 RepoInfo repoinfo.RepoInfo 900 Issue *db.Issue 901 Comment *db.Comment 902} 903 904func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 905 return p.executePlain("repo/issues/fragments/issueComment", w, params) 906} 907 908type RepoNewPullParams struct { 909 LoggedInUser *oauth.User 910 RepoInfo repoinfo.RepoInfo 911 Branches []types.Branch 912 Strategy string 913 SourceBranch string 914 TargetBranch string 915 Title string 916 Body string 917 Active string 918} 919 920func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 921 params.Active = "pulls" 922 return p.executeRepo("repo/pulls/new", w, params) 923} 924 925type RepoPullsParams struct { 926 LoggedInUser *oauth.User 927 RepoInfo repoinfo.RepoInfo 928 Pulls []*db.Pull 929 Active string 930 FilteringBy db.PullState 931 Stacks map[string]db.Stack 932 Pipelines map[string]db.Pipeline 933} 934 935func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 936 params.Active = "pulls" 937 return p.executeRepo("repo/pulls/pulls", w, params) 938} 939 940type ResubmitResult uint64 941 942const ( 943 ShouldResubmit ResubmitResult = iota 944 ShouldNotResubmit 945 Unknown 946) 947 948func (r ResubmitResult) Yes() bool { 949 return r == ShouldResubmit 950} 951func (r ResubmitResult) No() bool { 952 return r == ShouldNotResubmit 953} 954func (r ResubmitResult) Unknown() bool { 955 return r == Unknown 956} 957 958type RepoSinglePullParams struct { 959 LoggedInUser *oauth.User 960 RepoInfo repoinfo.RepoInfo 961 Active string 962 Pull *db.Pull 963 Stack db.Stack 964 AbandonedPulls []*db.Pull 965 MergeCheck types.MergeCheckResponse 966 ResubmitCheck ResubmitResult 967 Pipelines map[string]db.Pipeline 968 969 OrderedReactionKinds []db.ReactionKind 970 Reactions map[db.ReactionKind]int 971 UserReacted map[db.ReactionKind]bool 972} 973 974func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 975 params.Active = "pulls" 976 return p.executeRepo("repo/pulls/pull", w, params) 977} 978 979type RepoPullPatchParams struct { 980 LoggedInUser *oauth.User 981 RepoInfo repoinfo.RepoInfo 982 Pull *db.Pull 983 Stack db.Stack 984 Diff *types.NiceDiff 985 Round int 986 Submission *db.PullSubmission 987 OrderedReactionKinds []db.ReactionKind 988 DiffOpts types.DiffOpts 989} 990 991// this name is a mouthful 992func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 993 return p.execute("repo/pulls/patch", w, params) 994} 995 996type RepoPullInterdiffParams struct { 997 LoggedInUser *oauth.User 998 RepoInfo repoinfo.RepoInfo 999 Pull *db.Pull 1000 Round int 1001 Interdiff *patchutil.InterdiffResult 1002 OrderedReactionKinds []db.ReactionKind 1003 DiffOpts types.DiffOpts 1004} 1005 1006// this name is a mouthful 1007func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1008 return p.execute("repo/pulls/interdiff", w, params) 1009} 1010 1011type PullPatchUploadParams struct { 1012 RepoInfo repoinfo.RepoInfo 1013} 1014 1015func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1016 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1017} 1018 1019type PullCompareBranchesParams struct { 1020 RepoInfo repoinfo.RepoInfo 1021 Branches []types.Branch 1022 SourceBranch string 1023} 1024 1025func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1026 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1027} 1028 1029type PullCompareForkParams struct { 1030 RepoInfo repoinfo.RepoInfo 1031 Forks []db.Repo 1032 Selected string 1033} 1034 1035func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1036 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1037} 1038 1039type PullCompareForkBranchesParams struct { 1040 RepoInfo repoinfo.RepoInfo 1041 SourceBranches []types.Branch 1042 TargetBranches []types.Branch 1043} 1044 1045func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1046 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1047} 1048 1049type PullResubmitParams struct { 1050 LoggedInUser *oauth.User 1051 RepoInfo repoinfo.RepoInfo 1052 Pull *db.Pull 1053 SubmissionId int 1054} 1055 1056func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1057 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1058} 1059 1060type PullActionsParams struct { 1061 LoggedInUser *oauth.User 1062 RepoInfo repoinfo.RepoInfo 1063 Pull *db.Pull 1064 RoundNumber int 1065 MergeCheck types.MergeCheckResponse 1066 ResubmitCheck ResubmitResult 1067 Stack db.Stack 1068} 1069 1070func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1071 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1072} 1073 1074type PullNewCommentParams struct { 1075 LoggedInUser *oauth.User 1076 RepoInfo repoinfo.RepoInfo 1077 Pull *db.Pull 1078 RoundNumber int 1079} 1080 1081func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1082 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1083} 1084 1085type RepoCompareParams struct { 1086 LoggedInUser *oauth.User 1087 RepoInfo repoinfo.RepoInfo 1088 Forks []db.Repo 1089 Branches []types.Branch 1090 Tags []*types.TagReference 1091 Base string 1092 Head string 1093 Diff *types.NiceDiff 1094 DiffOpts types.DiffOpts 1095 1096 Active string 1097} 1098 1099func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1100 params.Active = "overview" 1101 return p.executeRepo("repo/compare/compare", w, params) 1102} 1103 1104type RepoCompareNewParams struct { 1105 LoggedInUser *oauth.User 1106 RepoInfo repoinfo.RepoInfo 1107 Forks []db.Repo 1108 Branches []types.Branch 1109 Tags []*types.TagReference 1110 Base string 1111 Head string 1112 1113 Active string 1114} 1115 1116func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1117 params.Active = "overview" 1118 return p.executeRepo("repo/compare/new", w, params) 1119} 1120 1121type RepoCompareAllowPullParams struct { 1122 LoggedInUser *oauth.User 1123 RepoInfo repoinfo.RepoInfo 1124 Base string 1125 Head string 1126} 1127 1128func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1129 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1130} 1131 1132type RepoCompareDiffParams struct { 1133 LoggedInUser *oauth.User 1134 RepoInfo repoinfo.RepoInfo 1135 Diff types.NiceDiff 1136} 1137 1138func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1139 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1140} 1141 1142type PipelinesParams struct { 1143 LoggedInUser *oauth.User 1144 RepoInfo repoinfo.RepoInfo 1145 Pipelines []db.Pipeline 1146 Active string 1147} 1148 1149func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1150 params.Active = "pipelines" 1151 return p.executeRepo("repo/pipelines/pipelines", w, params) 1152} 1153 1154type LogBlockParams struct { 1155 Id int 1156 Name string 1157 Command string 1158 Collapsed bool 1159} 1160 1161func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1162 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1163} 1164 1165type LogLineParams struct { 1166 Id int 1167 Content string 1168} 1169 1170func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1171 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1172} 1173 1174type WorkflowParams struct { 1175 LoggedInUser *oauth.User 1176 RepoInfo repoinfo.RepoInfo 1177 Pipeline db.Pipeline 1178 Workflow string 1179 LogUrl string 1180 Active string 1181} 1182 1183func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1184 params.Active = "pipelines" 1185 return p.executeRepo("repo/pipelines/workflow", w, params) 1186} 1187 1188type PutStringParams struct { 1189 LoggedInUser *oauth.User 1190 Action string 1191 1192 // this is supplied in the case of editing an existing string 1193 String db.String 1194} 1195 1196func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1197 return p.execute("strings/put", w, params) 1198} 1199 1200type StringsDashboardParams struct { 1201 LoggedInUser *oauth.User 1202 Card ProfileCard 1203 Strings []db.String 1204} 1205 1206func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1207 return p.execute("strings/dashboard", w, params) 1208} 1209 1210type StringTimelineParams struct { 1211 LoggedInUser *oauth.User 1212 Strings []db.String 1213} 1214 1215func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1216 return p.execute("strings/timeline", w, params) 1217} 1218 1219type SingleStringParams struct { 1220 LoggedInUser *oauth.User 1221 ShowRendered bool 1222 RenderToggle bool 1223 RenderedContents template.HTML 1224 String db.String 1225 Stats db.StringStats 1226 Owner identity.Identity 1227} 1228 1229func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1230 var style *chroma.Style = styles.Get("catpuccin-latte") 1231 1232 if params.ShowRendered { 1233 switch markup.GetFormat(params.String.Filename) { 1234 case markup.FormatMarkdown: 1235 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1236 htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1237 sanitized := p.rctx.SanitizeDefault(htmlString) 1238 params.RenderedContents = template.HTML(sanitized) 1239 } 1240 } 1241 1242 c := params.String.Contents 1243 formatter := chromahtml.New( 1244 chromahtml.InlineCode(false), 1245 chromahtml.WithLineNumbers(true), 1246 chromahtml.WithLinkableLineNumbers(true, "L"), 1247 chromahtml.Standalone(false), 1248 chromahtml.WithClasses(true), 1249 ) 1250 1251 lexer := lexers.Get(filepath.Base(params.String.Filename)) 1252 if lexer == nil { 1253 lexer = lexers.Fallback 1254 } 1255 1256 iterator, err := lexer.Tokenise(nil, c) 1257 if err != nil { 1258 return fmt.Errorf("chroma tokenize: %w", err) 1259 } 1260 1261 var code bytes.Buffer 1262 err = formatter.Format(&code, style, iterator) 1263 if err != nil { 1264 return fmt.Errorf("chroma format: %w", err) 1265 } 1266 1267 params.String.Contents = code.String() 1268 return p.execute("strings/string", w, params) 1269} 1270 1271func (p *Pages) Static() http.Handler { 1272 if p.dev { 1273 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1274 } 1275 1276 sub, err := fs.Sub(Files, "static") 1277 if err != nil { 1278 p.logger.Error("no static dir found? that's crazy", "err", err) 1279 panic(err) 1280 } 1281 // Custom handler to apply Cache-Control headers for font files 1282 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1283} 1284 1285func Cache(h http.Handler) http.Handler { 1286 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1287 path := strings.Split(r.URL.Path, "?")[0] 1288 1289 if strings.HasSuffix(path, ".css") { 1290 // on day for css files 1291 w.Header().Set("Cache-Control", "public, max-age=86400") 1292 } else { 1293 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1294 } 1295 h.ServeHTTP(w, r) 1296 }) 1297} 1298 1299func CssContentHash() string { 1300 cssFile, err := Files.Open("static/tw.css") 1301 if err != nil { 1302 slog.Debug("Error opening CSS file", "err", err) 1303 return "" 1304 } 1305 defer cssFile.Close() 1306 1307 hasher := sha256.New() 1308 if _, err := io.Copy(hasher, cssFile); err != nil { 1309 slog.Debug("Error hashing CSS file", "err", err) 1310 return "" 1311 } 1312 1313 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1314} 1315 1316func (p *Pages) Error500(w io.Writer) error { 1317 return p.execute("errors/500", w, nil) 1318} 1319 1320func (p *Pages) Error404(w io.Writer) error { 1321 return p.execute("errors/404", w, nil) 1322} 1323 1324func (p *Pages) ErrorKnot404(w io.Writer) error { 1325 return p.execute("errors/knot404", w, nil) 1326} 1327 1328func (p *Pages) Error503(w io.Writer) error { 1329 return p.execute("errors/503", w, nil) 1330}