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 447type ProfileReposParams struct { 448 LoggedInUser *oauth.User 449 Repos []db.Repo 450 Card *ProfileCard 451 Active string 452} 453 454func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 455 params.Active = "repos" 456 return p.executeProfile("user/repos", w, params) 457} 458 459type ProfileStarredParams struct { 460 LoggedInUser *oauth.User 461 Repos []db.Repo 462 Card *ProfileCard 463 Active string 464} 465 466func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 467 params.Active = "starred" 468 return p.executeProfile("user/starred", w, params) 469} 470 471type FollowCard struct { 472 UserDid string 473 FollowStatus db.FollowStatus 474 FollowersCount int 475 FollowingCount int 476 Profile *db.Profile 477} 478 479type ProfileFollowersParams struct { 480 LoggedInUser *oauth.User 481 Followers []FollowCard 482 Card *ProfileCard 483 Active string 484} 485 486func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 487 params.Active = "overview" 488 return p.executeProfile("user/followers", w, params) 489} 490 491type ProfileFollowingParams struct { 492 LoggedInUser *oauth.User 493 Following []FollowCard 494 Card *ProfileCard 495 Active string 496} 497 498func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 499 params.Active = "overview" 500 return p.executeProfile("user/following", w, params) 501} 502 503type FollowFragmentParams struct { 504 UserDid string 505 FollowStatus db.FollowStatus 506} 507 508func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 509 return p.executePlain("user/fragments/follow", w, params) 510} 511 512type EditBioParams struct { 513 LoggedInUser *oauth.User 514 Profile *db.Profile 515} 516 517func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 518 return p.executePlain("user/fragments/editBio", w, params) 519} 520 521type EditPinsParams struct { 522 LoggedInUser *oauth.User 523 Profile *db.Profile 524 AllRepos []PinnedRepo 525} 526 527type PinnedRepo struct { 528 IsPinned bool 529 db.Repo 530} 531 532func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 533 return p.executePlain("user/fragments/editPins", w, params) 534} 535 536type RepoStarFragmentParams struct { 537 IsStarred bool 538 RepoAt syntax.ATURI 539 Stats db.RepoStats 540} 541 542func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error { 543 return p.executePlain("repo/fragments/repoStar", w, params) 544} 545 546type RepoDescriptionParams struct { 547 RepoInfo repoinfo.RepoInfo 548} 549 550func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 551 return p.executePlain("repo/fragments/editRepoDescription", w, params) 552} 553 554func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 555 return p.executePlain("repo/fragments/repoDescription", w, params) 556} 557 558type RepoIndexParams struct { 559 LoggedInUser *oauth.User 560 RepoInfo repoinfo.RepoInfo 561 Active string 562 TagMap map[string][]string 563 CommitsTrunc []*object.Commit 564 TagsTrunc []*types.TagReference 565 BranchesTrunc []types.Branch 566 // ForkInfo *types.ForkInfo 567 HTMLReadme template.HTML 568 Raw bool 569 EmailToDidOrHandle map[string]string 570 VerifiedCommits commitverify.VerifiedCommits 571 Languages []types.RepoLanguageDetails 572 Pipelines map[string]db.Pipeline 573 types.RepoIndexResponse 574} 575 576func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 577 params.Active = "overview" 578 if params.IsEmpty { 579 return p.executeRepo("repo/empty", w, params) 580 } 581 582 p.rctx.RepoInfo = params.RepoInfo 583 p.rctx.RepoInfo.Ref = params.Ref 584 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 585 586 if params.ReadmeFileName != "" { 587 ext := filepath.Ext(params.ReadmeFileName) 588 switch ext { 589 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 590 params.Raw = false 591 htmlString := p.rctx.RenderMarkdown(params.Readme) 592 sanitized := p.rctx.SanitizeDefault(htmlString) 593 params.HTMLReadme = template.HTML(sanitized) 594 default: 595 params.Raw = true 596 } 597 } 598 599 return p.executeRepo("repo/index", w, params) 600} 601 602type RepoLogParams struct { 603 LoggedInUser *oauth.User 604 RepoInfo repoinfo.RepoInfo 605 TagMap map[string][]string 606 types.RepoLogResponse 607 Active string 608 EmailToDidOrHandle map[string]string 609 VerifiedCommits commitverify.VerifiedCommits 610 Pipelines map[string]db.Pipeline 611} 612 613func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 614 params.Active = "overview" 615 return p.executeRepo("repo/log", w, params) 616} 617 618type RepoCommitParams struct { 619 LoggedInUser *oauth.User 620 RepoInfo repoinfo.RepoInfo 621 Active string 622 EmailToDidOrHandle map[string]string 623 Pipeline *db.Pipeline 624 DiffOpts types.DiffOpts 625 626 // singular because it's always going to be just one 627 VerifiedCommit commitverify.VerifiedCommits 628 629 types.RepoCommitResponse 630} 631 632func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 633 params.Active = "overview" 634 return p.executeRepo("repo/commit", w, params) 635} 636 637type RepoTreeParams struct { 638 LoggedInUser *oauth.User 639 RepoInfo repoinfo.RepoInfo 640 Active string 641 BreadCrumbs [][]string 642 TreePath string 643 types.RepoTreeResponse 644} 645 646type RepoTreeStats struct { 647 NumFolders uint64 648 NumFiles uint64 649} 650 651func (r RepoTreeParams) TreeStats() RepoTreeStats { 652 numFolders, numFiles := 0, 0 653 for _, f := range r.Files { 654 if !f.IsFile { 655 numFolders += 1 656 } else if f.IsFile { 657 numFiles += 1 658 } 659 } 660 661 return RepoTreeStats{ 662 NumFolders: uint64(numFolders), 663 NumFiles: uint64(numFiles), 664 } 665} 666 667func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 668 params.Active = "overview" 669 return p.executeRepo("repo/tree", w, params) 670} 671 672type RepoBranchesParams struct { 673 LoggedInUser *oauth.User 674 RepoInfo repoinfo.RepoInfo 675 Active string 676 types.RepoBranchesResponse 677} 678 679func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 680 params.Active = "overview" 681 return p.executeRepo("repo/branches", w, params) 682} 683 684type RepoTagsParams struct { 685 LoggedInUser *oauth.User 686 RepoInfo repoinfo.RepoInfo 687 Active string 688 types.RepoTagsResponse 689 ArtifactMap map[plumbing.Hash][]db.Artifact 690 DanglingArtifacts []db.Artifact 691} 692 693func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 694 params.Active = "overview" 695 return p.executeRepo("repo/tags", w, params) 696} 697 698type RepoArtifactParams struct { 699 LoggedInUser *oauth.User 700 RepoInfo repoinfo.RepoInfo 701 Artifact db.Artifact 702} 703 704func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 705 return p.executePlain("repo/fragments/artifact", w, params) 706} 707 708type RepoBlobParams struct { 709 LoggedInUser *oauth.User 710 RepoInfo repoinfo.RepoInfo 711 Active string 712 Unsupported bool 713 IsImage bool 714 IsVideo bool 715 ContentSrc string 716 BreadCrumbs [][]string 717 ShowRendered bool 718 RenderToggle bool 719 RenderedContents template.HTML 720 types.RepoBlobResponse 721} 722 723func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 724 var style *chroma.Style = styles.Get("catpuccin-latte") 725 726 if params.ShowRendered { 727 switch markup.GetFormat(params.Path) { 728 case markup.FormatMarkdown: 729 p.rctx.RepoInfo = params.RepoInfo 730 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 731 htmlString := p.rctx.RenderMarkdown(params.Contents) 732 sanitized := p.rctx.SanitizeDefault(htmlString) 733 params.RenderedContents = template.HTML(sanitized) 734 } 735 } 736 737 c := params.Contents 738 formatter := chromahtml.New( 739 chromahtml.InlineCode(false), 740 chromahtml.WithLineNumbers(true), 741 chromahtml.WithLinkableLineNumbers(true, "L"), 742 chromahtml.Standalone(false), 743 chromahtml.WithClasses(true), 744 ) 745 746 lexer := lexers.Get(filepath.Base(params.Path)) 747 if lexer == nil { 748 lexer = lexers.Fallback 749 } 750 751 iterator, err := lexer.Tokenise(nil, c) 752 if err != nil { 753 return fmt.Errorf("chroma tokenize: %w", err) 754 } 755 756 var code bytes.Buffer 757 err = formatter.Format(&code, style, iterator) 758 if err != nil { 759 return fmt.Errorf("chroma format: %w", err) 760 } 761 762 params.Contents = code.String() 763 params.Active = "overview" 764 return p.executeRepo("repo/blob", w, params) 765} 766 767type Collaborator struct { 768 Did string 769 Handle string 770 Role string 771} 772 773type RepoSettingsParams struct { 774 LoggedInUser *oauth.User 775 RepoInfo repoinfo.RepoInfo 776 Collaborators []Collaborator 777 Active string 778 Branches []types.Branch 779 Spindles []string 780 CurrentSpindle string 781 Secrets []*tangled.RepoListSecrets_Secret 782 783 // TODO: use repoinfo.roles 784 IsCollaboratorInviteAllowed bool 785} 786 787func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 788 params.Active = "settings" 789 return p.executeRepo("repo/settings", w, params) 790} 791 792type RepoGeneralSettingsParams struct { 793 LoggedInUser *oauth.User 794 RepoInfo repoinfo.RepoInfo 795 Active string 796 Tabs []map[string]any 797 Tab string 798 Branches []types.Branch 799} 800 801func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 802 params.Active = "settings" 803 return p.executeRepo("repo/settings/general", w, params) 804} 805 806type RepoAccessSettingsParams struct { 807 LoggedInUser *oauth.User 808 RepoInfo repoinfo.RepoInfo 809 Active string 810 Tabs []map[string]any 811 Tab string 812 Collaborators []Collaborator 813} 814 815func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 816 params.Active = "settings" 817 return p.executeRepo("repo/settings/access", w, params) 818} 819 820type RepoPipelineSettingsParams struct { 821 LoggedInUser *oauth.User 822 RepoInfo repoinfo.RepoInfo 823 Active string 824 Tabs []map[string]any 825 Tab string 826 Spindles []string 827 CurrentSpindle string 828 Secrets []map[string]any 829} 830 831func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 832 params.Active = "settings" 833 return p.executeRepo("repo/settings/pipelines", w, params) 834} 835 836type RepoIssuesParams struct { 837 LoggedInUser *oauth.User 838 RepoInfo repoinfo.RepoInfo 839 Active string 840 Issues []db.Issue 841 Page pagination.Page 842 FilteringByOpen bool 843} 844 845func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 846 params.Active = "issues" 847 return p.executeRepo("repo/issues/issues", w, params) 848} 849 850type RepoSingleIssueParams struct { 851 LoggedInUser *oauth.User 852 RepoInfo repoinfo.RepoInfo 853 Active string 854 Issue *db.Issue 855 Comments []db.Comment 856 IssueOwnerHandle string 857 858 OrderedReactionKinds []db.ReactionKind 859 Reactions map[db.ReactionKind]int 860 UserReacted map[db.ReactionKind]bool 861 862 State string 863} 864 865type ThreadReactionFragmentParams struct { 866 ThreadAt syntax.ATURI 867 Kind db.ReactionKind 868 Count int 869 IsReacted bool 870} 871 872func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 873 return p.executePlain("repo/fragments/reaction", w, params) 874} 875 876func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 877 params.Active = "issues" 878 if params.Issue.Open { 879 params.State = "open" 880 } else { 881 params.State = "closed" 882 } 883 return p.executeRepo("repo/issues/issue", w, params) 884} 885 886type RepoNewIssueParams struct { 887 LoggedInUser *oauth.User 888 RepoInfo repoinfo.RepoInfo 889 Active string 890} 891 892func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 893 params.Active = "issues" 894 return p.executeRepo("repo/issues/new", w, params) 895} 896 897type EditIssueCommentParams struct { 898 LoggedInUser *oauth.User 899 RepoInfo repoinfo.RepoInfo 900 Issue *db.Issue 901 Comment *db.Comment 902} 903 904func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 905 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 906} 907 908type SingleIssueCommentParams struct { 909 LoggedInUser *oauth.User 910 RepoInfo repoinfo.RepoInfo 911 Issue *db.Issue 912 Comment *db.Comment 913} 914 915func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 916 return p.executePlain("repo/issues/fragments/issueComment", w, params) 917} 918 919type RepoNewPullParams struct { 920 LoggedInUser *oauth.User 921 RepoInfo repoinfo.RepoInfo 922 Branches []types.Branch 923 Strategy string 924 SourceBranch string 925 TargetBranch string 926 Title string 927 Body string 928 Active string 929} 930 931func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 932 params.Active = "pulls" 933 return p.executeRepo("repo/pulls/new", w, params) 934} 935 936type RepoPullsParams struct { 937 LoggedInUser *oauth.User 938 RepoInfo repoinfo.RepoInfo 939 Pulls []*db.Pull 940 Active string 941 FilteringBy db.PullState 942 Stacks map[string]db.Stack 943 Pipelines map[string]db.Pipeline 944} 945 946func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 947 params.Active = "pulls" 948 return p.executeRepo("repo/pulls/pulls", w, params) 949} 950 951type ResubmitResult uint64 952 953const ( 954 ShouldResubmit ResubmitResult = iota 955 ShouldNotResubmit 956 Unknown 957) 958 959func (r ResubmitResult) Yes() bool { 960 return r == ShouldResubmit 961} 962func (r ResubmitResult) No() bool { 963 return r == ShouldNotResubmit 964} 965func (r ResubmitResult) Unknown() bool { 966 return r == Unknown 967} 968 969type RepoSinglePullParams struct { 970 LoggedInUser *oauth.User 971 RepoInfo repoinfo.RepoInfo 972 Active string 973 Pull *db.Pull 974 Stack db.Stack 975 AbandonedPulls []*db.Pull 976 MergeCheck types.MergeCheckResponse 977 ResubmitCheck ResubmitResult 978 Pipelines map[string]db.Pipeline 979 980 OrderedReactionKinds []db.ReactionKind 981 Reactions map[db.ReactionKind]int 982 UserReacted map[db.ReactionKind]bool 983} 984 985func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 986 params.Active = "pulls" 987 return p.executeRepo("repo/pulls/pull", w, params) 988} 989 990type RepoPullPatchParams struct { 991 LoggedInUser *oauth.User 992 RepoInfo repoinfo.RepoInfo 993 Pull *db.Pull 994 Stack db.Stack 995 Diff *types.NiceDiff 996 Round int 997 Submission *db.PullSubmission 998 OrderedReactionKinds []db.ReactionKind 999 DiffOpts types.DiffOpts 1000} 1001 1002// this name is a mouthful 1003func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1004 return p.execute("repo/pulls/patch", w, params) 1005} 1006 1007type RepoPullInterdiffParams struct { 1008 LoggedInUser *oauth.User 1009 RepoInfo repoinfo.RepoInfo 1010 Pull *db.Pull 1011 Round int 1012 Interdiff *patchutil.InterdiffResult 1013 OrderedReactionKinds []db.ReactionKind 1014 DiffOpts types.DiffOpts 1015} 1016 1017// this name is a mouthful 1018func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1019 return p.execute("repo/pulls/interdiff", w, params) 1020} 1021 1022type PullPatchUploadParams struct { 1023 RepoInfo repoinfo.RepoInfo 1024} 1025 1026func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1027 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1028} 1029 1030type PullCompareBranchesParams struct { 1031 RepoInfo repoinfo.RepoInfo 1032 Branches []types.Branch 1033 SourceBranch string 1034} 1035 1036func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1037 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1038} 1039 1040type PullCompareForkParams struct { 1041 RepoInfo repoinfo.RepoInfo 1042 Forks []db.Repo 1043 Selected string 1044} 1045 1046func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1047 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1048} 1049 1050type PullCompareForkBranchesParams struct { 1051 RepoInfo repoinfo.RepoInfo 1052 SourceBranches []types.Branch 1053 TargetBranches []types.Branch 1054} 1055 1056func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1057 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1058} 1059 1060type PullResubmitParams struct { 1061 LoggedInUser *oauth.User 1062 RepoInfo repoinfo.RepoInfo 1063 Pull *db.Pull 1064 SubmissionId int 1065} 1066 1067func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1068 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1069} 1070 1071type PullActionsParams struct { 1072 LoggedInUser *oauth.User 1073 RepoInfo repoinfo.RepoInfo 1074 Pull *db.Pull 1075 RoundNumber int 1076 MergeCheck types.MergeCheckResponse 1077 ResubmitCheck ResubmitResult 1078 Stack db.Stack 1079} 1080 1081func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1082 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1083} 1084 1085type PullNewCommentParams struct { 1086 LoggedInUser *oauth.User 1087 RepoInfo repoinfo.RepoInfo 1088 Pull *db.Pull 1089 RoundNumber int 1090} 1091 1092func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1093 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1094} 1095 1096type RepoCompareParams struct { 1097 LoggedInUser *oauth.User 1098 RepoInfo repoinfo.RepoInfo 1099 Forks []db.Repo 1100 Branches []types.Branch 1101 Tags []*types.TagReference 1102 Base string 1103 Head string 1104 Diff *types.NiceDiff 1105 DiffOpts types.DiffOpts 1106 1107 Active string 1108} 1109 1110func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1111 params.Active = "overview" 1112 return p.executeRepo("repo/compare/compare", w, params) 1113} 1114 1115type RepoCompareNewParams struct { 1116 LoggedInUser *oauth.User 1117 RepoInfo repoinfo.RepoInfo 1118 Forks []db.Repo 1119 Branches []types.Branch 1120 Tags []*types.TagReference 1121 Base string 1122 Head string 1123 1124 Active string 1125} 1126 1127func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1128 params.Active = "overview" 1129 return p.executeRepo("repo/compare/new", w, params) 1130} 1131 1132type RepoCompareAllowPullParams struct { 1133 LoggedInUser *oauth.User 1134 RepoInfo repoinfo.RepoInfo 1135 Base string 1136 Head string 1137} 1138 1139func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1140 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1141} 1142 1143type RepoCompareDiffParams struct { 1144 LoggedInUser *oauth.User 1145 RepoInfo repoinfo.RepoInfo 1146 Diff types.NiceDiff 1147} 1148 1149func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1150 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1151} 1152 1153type PipelinesParams struct { 1154 LoggedInUser *oauth.User 1155 RepoInfo repoinfo.RepoInfo 1156 Pipelines []db.Pipeline 1157 Active string 1158} 1159 1160func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1161 params.Active = "pipelines" 1162 return p.executeRepo("repo/pipelines/pipelines", w, params) 1163} 1164 1165type LogBlockParams struct { 1166 Id int 1167 Name string 1168 Command string 1169 Collapsed bool 1170} 1171 1172func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1173 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1174} 1175 1176type LogLineParams struct { 1177 Id int 1178 Content string 1179} 1180 1181func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1182 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1183} 1184 1185type WorkflowParams struct { 1186 LoggedInUser *oauth.User 1187 RepoInfo repoinfo.RepoInfo 1188 Pipeline db.Pipeline 1189 Workflow string 1190 LogUrl string 1191 Active string 1192} 1193 1194func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1195 params.Active = "pipelines" 1196 return p.executeRepo("repo/pipelines/workflow", w, params) 1197} 1198 1199type PutStringParams struct { 1200 LoggedInUser *oauth.User 1201 Action string 1202 1203 // this is supplied in the case of editing an existing string 1204 String db.String 1205} 1206 1207func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1208 return p.execute("strings/put", w, params) 1209} 1210 1211type StringsDashboardParams struct { 1212 LoggedInUser *oauth.User 1213 Card ProfileCard 1214 Strings []db.String 1215} 1216 1217func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1218 return p.execute("strings/dashboard", w, params) 1219} 1220 1221type StringTimelineParams struct { 1222 LoggedInUser *oauth.User 1223 Strings []db.String 1224} 1225 1226func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1227 return p.execute("strings/timeline", w, params) 1228} 1229 1230type SingleStringParams struct { 1231 LoggedInUser *oauth.User 1232 ShowRendered bool 1233 RenderToggle bool 1234 RenderedContents template.HTML 1235 String db.String 1236 Stats db.StringStats 1237 Owner identity.Identity 1238} 1239 1240func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1241 var style *chroma.Style = styles.Get("catpuccin-latte") 1242 1243 if params.ShowRendered { 1244 switch markup.GetFormat(params.String.Filename) { 1245 case markup.FormatMarkdown: 1246 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1247 htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1248 sanitized := p.rctx.SanitizeDefault(htmlString) 1249 params.RenderedContents = template.HTML(sanitized) 1250 } 1251 } 1252 1253 c := params.String.Contents 1254 formatter := chromahtml.New( 1255 chromahtml.InlineCode(false), 1256 chromahtml.WithLineNumbers(true), 1257 chromahtml.WithLinkableLineNumbers(true, "L"), 1258 chromahtml.Standalone(false), 1259 chromahtml.WithClasses(true), 1260 ) 1261 1262 lexer := lexers.Get(filepath.Base(params.String.Filename)) 1263 if lexer == nil { 1264 lexer = lexers.Fallback 1265 } 1266 1267 iterator, err := lexer.Tokenise(nil, c) 1268 if err != nil { 1269 return fmt.Errorf("chroma tokenize: %w", err) 1270 } 1271 1272 var code bytes.Buffer 1273 err = formatter.Format(&code, style, iterator) 1274 if err != nil { 1275 return fmt.Errorf("chroma format: %w", err) 1276 } 1277 1278 params.String.Contents = code.String() 1279 return p.execute("strings/string", w, params) 1280} 1281 1282func (p *Pages) Static() http.Handler { 1283 if p.dev { 1284 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1285 } 1286 1287 sub, err := fs.Sub(Files, "static") 1288 if err != nil { 1289 p.logger.Error("no static dir found? that's crazy", "err", err) 1290 panic(err) 1291 } 1292 // Custom handler to apply Cache-Control headers for font files 1293 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1294} 1295 1296func Cache(h http.Handler) http.Handler { 1297 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1298 path := strings.Split(r.URL.Path, "?")[0] 1299 1300 if strings.HasSuffix(path, ".css") { 1301 // on day for css files 1302 w.Header().Set("Cache-Control", "public, max-age=86400") 1303 } else { 1304 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1305 } 1306 h.ServeHTTP(w, r) 1307 }) 1308} 1309 1310func CssContentHash() string { 1311 cssFile, err := Files.Open("static/tw.css") 1312 if err != nil { 1313 slog.Debug("Error opening CSS file", "err", err) 1314 return "" 1315 } 1316 defer cssFile.Close() 1317 1318 hasher := sha256.New() 1319 if _, err := io.Copy(hasher, cssFile); err != nil { 1320 slog.Debug("Error hashing CSS file", "err", err) 1321 return "" 1322 } 1323 1324 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1325} 1326 1327func (p *Pages) Error500(w io.Writer) error { 1328 return p.execute("errors/500", w, nil) 1329} 1330 1331func (p *Pages) Error404(w io.Writer) error { 1332 return p.execute("errors/404", w, nil) 1333} 1334 1335func (p *Pages) ErrorKnot404(w io.Writer) error { 1336 return p.execute("errors/knot404", w, nil) 1337} 1338 1339func (p *Pages) Error503(w io.Writer) error { 1340 return p.execute("errors/503", w, nil) 1341}