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