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