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