Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / pages.go
51 kB 1986 lines
1package pages 2 3import ( 4 "context" 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/cache" 22 "tangled.org/core/appview/commitverify" 23 "tangled.org/core/appview/config" 24 "tangled.org/core/appview/db" 25 "tangled.org/core/appview/models" 26 "tangled.org/core/appview/oauth" 27 "tangled.org/core/appview/pages/markup" 28 "tangled.org/core/appview/pages/markup/sanitizer" 29 "tangled.org/core/appview/pages/repoinfo" 30 "tangled.org/core/appview/pagination" 31 "tangled.org/core/idresolver" 32 "tangled.org/core/types" 33 34 "github.com/bluesky-social/indigo/atproto/identity" 35 "github.com/bluesky-social/indigo/atproto/syntax" 36 "github.com/go-git/go-git/v5/plumbing" 37 "github.com/sourcegraph/zoekt" 38) 39 40//go:embed templates/* static legal 41var Files embed.FS 42 43type baseParamsCtxKey struct{} 44 45type BaseParams struct { 46 LoggedInUser *oauth.MultiAccountUser 47 FocusParams FocusParams 48} 49 50type FocusParams struct { 51 Focusing bool 52 FocusLink string 53 FocusNotificationID int64 54 CurrentPath string // r.URL.Path, for off-focus detection in templates 55 FocusCount int // total unread focus-eligible items remaining 56} 57 58func (p *Pages) Resolver() *idresolver.Resolver { 59 return p.resolver 60} 61 62func BaseParamsIntoContext(ctx context.Context, bp BaseParams) context.Context { 63 return context.WithValue(ctx, baseParamsCtxKey{}, bp) 64} 65 66func BaseParamsFromContext(ctx context.Context) BaseParams { 67 bp, _ := ctx.Value(baseParamsCtxKey{}).(BaseParams) 68 return bp 69} 70 71type Pages struct { 72 mu sync.RWMutex 73 cache *TmplCache[string, *template.Template] 74 75 avatar config.AvatarConfig 76 pdsCfg config.PdsConfig 77 resolver *idresolver.Resolver 78 db *db.DB 79 rdb *cache.Cache 80 dev bool 81 embedFS fs.FS 82 templateDir string // Path to templates on disk for dev mode 83 rctx *markup.RenderContext 84 logger *slog.Logger 85} 86 87func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 88 // initialized with safe defaults, can be overridden per use 89 rctx := &markup.RenderContext{ 90 IsDev: config.Core.Dev, 91 Hostname: config.Core.AppviewHost, 92 CamoUrl: config.Camo.Host, 93 CamoSecret: config.Camo.SharedSecret, 94 Files: Files, 95 } 96 97 p := &Pages{ 98 mu: sync.RWMutex{}, 99 cache: NewTmplCache[string, *template.Template](), 100 dev: config.Core.Dev, 101 avatar: config.Avatar, 102 pdsCfg: config.Pds, 103 rctx: rctx, 104 resolver: res, 105 db: database, 106 rdb: rdb, 107 templateDir: "appview/pages", 108 logger: logger, 109 } 110 111 if p.dev { 112 p.embedFS = os.DirFS(p.templateDir) 113 } else { 114 p.embedFS = Files 115 } 116 117 return p 118} 119 120// reverse of pathToName 121func (p *Pages) nameToPath(s string) string { 122 return "templates/" + s + ".html" 123} 124 125// FuncMap returns the template function map for use by external template consumers. 126func (p *Pages) FuncMap() template.FuncMap { 127 return p.funcMap() 128} 129 130// FragmentPaths returns all fragment template paths from the embedded FS. 131func (p *Pages) FragmentPaths() ([]string, error) { 132 return p.fragmentPaths() 133} 134 135// EmbedFS returns the embedded filesystem containing templates and static assets. 136func (p *Pages) EmbedFS() fs.FS { 137 return p.embedFS 138} 139 140func (p *Pages) fragmentPaths() ([]string, error) { 141 var fragmentPaths []string 142 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 143 if err != nil { 144 return err 145 } 146 if d.IsDir() { 147 return nil 148 } 149 if !strings.HasSuffix(path, ".html") { 150 return nil 151 } 152 if !strings.Contains(path, "fragments/") { 153 return nil 154 } 155 fragmentPaths = append(fragmentPaths, path) 156 return nil 157 }) 158 if err != nil { 159 return nil, err 160 } 161 162 return fragmentPaths, nil 163} 164 165// parse without memoization 166func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 167 paths, err := p.fragmentPaths() 168 if err != nil { 169 return nil, err 170 } 171 for _, s := range stack { 172 paths = append(paths, p.nameToPath(s)) 173 } 174 175 funcs := p.funcMap() 176 top := stack[len(stack)-1] 177 parsed, err := template.New(top). 178 Funcs(funcs). 179 ParseFS(p.embedFS, paths...) 180 if err != nil { 181 return nil, err 182 } 183 184 return parsed, nil 185} 186 187func (p *Pages) parse(stack ...string) (*template.Template, error) { 188 key := strings.Join(stack, "|") 189 190 // never cache in dev mode 191 if cached, exists := p.cache.Get(key); !p.dev && exists { 192 return cached, nil 193 } 194 195 result, err := p.rawParse(stack...) 196 if err != nil { 197 return nil, err 198 } 199 200 p.cache.Set(key, result) 201 return result, nil 202} 203 204func (p *Pages) parseBase(top string) (*template.Template, error) { 205 stack := []string{ 206 "layouts/base", 207 top, 208 } 209 return p.parse(stack...) 210} 211 212func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 213 stack := []string{ 214 "layouts/base", 215 "layouts/repobase", 216 top, 217 } 218 return p.parse(stack...) 219} 220 221func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 222 stack := []string{ 223 "layouts/base", 224 "layouts/profilebase", 225 top, 226 } 227 return p.parse(stack...) 228} 229 230func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 231 stack := []string{ 232 "layouts/base", 233 "layouts/loginbase", 234 top, 235 } 236 return p.parse(stack...) 237} 238 239func (p *Pages) executePlain(name string, w io.Writer, params any) error { 240 tpl, err := p.parse(name) 241 if err != nil { 242 return err 243 } 244 245 return tpl.Execute(w, params) 246} 247 248func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 249 tpl, err := p.parseLoginBase(name) 250 if err != nil { 251 return err 252 } 253 254 return tpl.ExecuteTemplate(w, "layouts/base", params) 255} 256 257func (p *Pages) execute(name string, w io.Writer, params any) error { 258 tpl, err := p.parseBase(name) 259 if err != nil { 260 return err 261 } 262 263 return tpl.ExecuteTemplate(w, "layouts/base", params) 264} 265 266func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 267 tpl, err := p.parseRepoBase(name) 268 if err != nil { 269 return err 270 } 271 272 return tpl.ExecuteTemplate(w, "layouts/base", params) 273} 274 275func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 276 tpl, err := p.parseProfileBase(name) 277 if err != nil { 278 return err 279 } 280 281 return tpl.ExecuteTemplate(w, "layouts/base", params) 282} 283 284type DollyParams struct { 285 Classes string 286 FillColor string 287 // Favicon embeds a prefers-color-scheme style block so the SVG 288 // adapts to dark mode when used as a standalone favicon document. 289 Favicon bool 290} 291 292func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 293 return p.executePlain("fragments/dolly/logo", w, params) 294} 295 296func (p *Pages) Favicon(w io.Writer) error { 297 return p.Dolly(w, DollyParams{ 298 Favicon: true, 299 }) 300} 301 302type LoginParams struct { 303 ReturnUrl string 304 ErrorCode string 305 AddAccount bool 306 Accounts []oauth.AccountInfo 307} 308 309func (p *Pages) Login(w io.Writer, params LoginParams) error { 310 return p.executeLogin("user/login", w, params) 311} 312 313type SignupParams struct { 314 CloudflareSiteKey string 315 EmailId string 316} 317 318func (p *Pages) Signup(w io.Writer, params SignupParams) error { 319 return p.executeLogin("user/signup", w, params) 320} 321 322func (p *Pages) CompleteSignup(w io.Writer) error { 323 return p.executeLogin("user/completeSignup", w, nil) 324} 325 326type TermsOfServiceParams struct { 327 BaseParams 328 Content template.HTML 329} 330 331func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 332 filename := "terms.md" 333 filePath := filepath.Join("legal", filename) 334 335 file, err := p.embedFS.Open(filePath) 336 if err != nil { 337 return fmt.Errorf("failed to read %s: %w", filename, err) 338 } 339 defer file.Close() 340 341 markdownBytes, err := io.ReadAll(file) 342 if err != nil { 343 return fmt.Errorf("failed to read %s: %w", filename, err) 344 } 345 346 rctx := p.rctx.Clone() 347 rctx.RendererType = markup.RendererTypeDefault 348 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 349 sanitized := sanitizer.SanitizeDefault(htmlString) 350 params.Content = template.HTML(sanitized) 351 352 return p.execute("legal/terms", w, params) 353} 354 355type PrivacyPolicyParams struct { 356 BaseParams 357 Content template.HTML 358} 359 360func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 361 filename := "privacy.md" 362 filePath := filepath.Join("legal", filename) 363 364 file, err := p.embedFS.Open(filePath) 365 if err != nil { 366 return fmt.Errorf("failed to read %s: %w", filename, err) 367 } 368 defer file.Close() 369 370 markdownBytes, err := io.ReadAll(file) 371 if err != nil { 372 return fmt.Errorf("failed to read %s: %w", filename, err) 373 } 374 375 rctx := p.rctx.Clone() 376 rctx.RendererType = markup.RendererTypeDefault 377 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 378 sanitized := sanitizer.SanitizeDefault(htmlString) 379 params.Content = template.HTML(sanitized) 380 381 return p.execute("legal/privacy", w, params) 382} 383 384type BrandParams struct { 385 BaseParams 386} 387 388func (p *Pages) Brand(w io.Writer, params BrandParams) error { 389 return p.execute("brand/brand", w, params) 390} 391 392type RecentItem struct { 393 Link *models.RecentLink 394 Repo *models.Repo 395 Issue *models.Issue 396 Pull *models.Pull 397} 398 399type BlogPost struct { 400 Slug string 401 Title string 402 Subtitle string 403 Date time.Time 404} 405 406type TimelineParams struct { 407 BaseParams 408 Timeline []models.TimelineGroup 409 Repos []models.Repo 410 GfiLabel *models.LabelDefinition 411 BlueskyPosts []models.BskyPost 412 VouchSuggestions []models.VouchSuggestion 413 Notifications []*models.NotificationWithEntity 414 Recents []RecentItem 415 FollowingOnly bool 416 RecentBlogPosts []BlogPost 417 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 418 // For logged-in users it reflects their newsletter_preferences row; for 419 // anonymous visitors it is always true (dismissal falls back to 420 // localStorage on the client). 421 ShowNewsletter bool 422 CanFocus bool 423} 424 425func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 426 return p.execute("timeline/timeline", w, params) 427} 428 429type GoodFirstIssuesParams struct { 430 BaseParams 431 Issues []models.Issue 432 RepoGroups []*models.RepoGroup 433 LabelDefs map[string]*models.LabelDefinition 434 GfiLabel *models.LabelDefinition 435 Page pagination.Page 436} 437 438func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 439 return p.execute("goodfirstissues/index", w, params) 440} 441 442type UserProfileSettingsParams struct { 443 BaseParams 444 Tab string 445 PunchcardPreference models.PunchcardPreference 446 IsTnglSh bool 447 IsDeactivated bool 448 HandleOpen bool 449} 450 451func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 452 params.Tab = "profile" 453 return p.execute("user/settings/profile", w, params) 454} 455 456type GroupedNotifications struct { 457 Today []*models.NotificationWithEntity 458 ThisWeek []*models.NotificationWithEntity 459 Older []*models.NotificationWithEntity 460} 461 462func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications { 463 now := time.Now() 464 todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 465 weekStart := todayStart.AddDate(0, 0, -6) 466 467 var g GroupedNotifications 468 for _, n := range notifs { 469 switch { 470 case !n.Created.Before(todayStart): 471 g.Today = append(g.Today, n) 472 case !n.Created.Before(weekStart): 473 g.ThisWeek = append(g.ThisWeek, n) 474 default: 475 g.Older = append(g.Older, n) 476 } 477 } 478 return g 479} 480 481type NotificationsParams struct { 482 BaseParams 483 WorkGroups GroupedNotifications 484 SocialGroups GroupedNotifications 485 MobileGroups GroupedNotifications 486 WorkUnreadCount int64 487 SocialUnreadCount int64 488 Page pagination.Page 489 Total int 490 ReadFilter string // "inbox" or "unread" 491 CategoryFilter string // "all", "work", "social" 492 CanFocus bool 493} 494 495func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 496 return p.execute("notifications/list", w, params) 497} 498 499func (p *Pages) NotificationItem(w io.Writer, notif *models.NotificationWithEntity) error { 500 return p.executePlain("notifications/fragments/item", w, notif) 501} 502 503type NotificationCountParams struct { 504 Count int64 505} 506 507func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 508 return p.executePlain("notifications/fragments/count", w, params) 509} 510 511type NotificationPreviewParams struct { 512 BaseParams 513 Notifications []*models.NotificationWithEntity 514 ReadFilter string 515 CategoryFilter string 516 CanFocus bool 517} 518 519func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error { 520 return p.executePlain("notifications/fragments/preview", w, params) 521} 522 523type UserKeysSettingsParams struct { 524 BaseParams 525 PubKeys []models.PublicKey 526 Tab string 527} 528 529func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 530 params.Tab = "keys" 531 return p.execute("user/settings/keys", w, params) 532} 533 534type UserEmailsSettingsParams struct { 535 BaseParams 536 Emails []models.Email 537 Tab string 538} 539 540func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 541 params.Tab = "emails" 542 return p.execute("user/settings/emails", w, params) 543} 544 545type UserNotificationSettingsParams struct { 546 BaseParams 547 Preferences *models.NotificationPreferences 548 Tab string 549} 550 551func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 552 params.Tab = "notifications" 553 return p.execute("user/settings/notifications", w, params) 554} 555 556type UserSiteSettingsParams struct { 557 BaseParams 558 Claim *models.DomainClaim 559 SitesDomain string 560 IsTnglHandle bool 561 Tab string 562} 563 564func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 565 params.Tab = "sites" 566 return p.execute("user/settings/sites", w, params) 567} 568 569type UpgradeBannerParams struct { 570 Registrations []models.Registration 571 Spindles []models.Spindle 572} 573 574func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 575 return p.executePlain("banner", w, params) 576} 577 578type NewsletterResponseParams struct { 579 // Id identifies the calling form instance; the response span's id will 580 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 581 Id string 582 // Error, when non-empty, switches the template to the error variant. 583 Error string 584} 585 586func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 587 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 588} 589 590type KnotsParams struct { 591 BaseParams 592 Knots []KnotListingParams 593 Tab string 594} 595 596func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 597 params.Tab = "knots" 598 return p.execute("knots/index", w, params) 599} 600 601type KnotParams struct { 602 BaseParams 603 Registration *models.Registration 604 Members []string 605 Repos map[string][]models.Repo 606 IsOwner bool 607 RepoCount int 608 Tab string 609} 610 611func (p *Pages) Knot(w io.Writer, params KnotParams) error { 612 return p.execute("knots/dashboard", w, params) 613} 614 615type KnotListingParams struct { 616 *models.Registration 617 RepoCount int 618} 619 620func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 621 return p.executePlain("knots/fragments/knotListing", w, params) 622} 623 624type SpindlesParams struct { 625 BaseParams 626 Spindles []models.Spindle 627 Tab string 628} 629 630func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 631 params.Tab = "spindles" 632 return p.execute("spindles/index", w, params) 633} 634 635type SpindleListingParams struct { 636 models.Spindle 637 Tab string 638} 639 640func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 641 return p.executePlain("spindles/fragments/spindleListing", w, params) 642} 643 644type SpindleDashboardParams struct { 645 BaseParams 646 Spindle models.Spindle 647 Members []string 648 Repos map[string][]models.Repo 649 Tab string 650} 651 652func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 653 return p.execute("spindles/dashboard", w, params) 654} 655 656type NewRepoParams struct { 657 BaseParams 658 Knots []string 659} 660 661func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 662 return p.execute("repo/new", w, params) 663} 664 665type ForkRepoParams struct { 666 BaseParams 667 Knots []string 668 RepoInfo repoinfo.RepoInfo 669} 670 671func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 672 return p.execute("repo/fork", w, params) 673} 674 675type ProfileCard struct { 676 UserDid string 677 HasProfile bool 678 FollowStatus models.FollowStatus 679 VouchRelationship *models.VouchRelationship 680 Punchcard *models.Punchcard 681 Profile *models.Profile 682 Stats ProfileStats 683 Active string 684} 685 686type ProfileStats struct { 687 RepoCount int64 688 StarredCount int64 689 StringCount int64 690 FollowersCount int64 691 FollowingCount int64 692} 693 694func (p *ProfileCard) GetTabs() [][]any { 695 tabs := [][]any{ 696 {"overview", "overview", "square-chart-gantt", nil}, 697 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 698 {"starred", "starred", "star", p.Stats.StarredCount}, 699 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 700 {"vouches", "vouches", "shield", nil}, 701 } 702 703 return tabs 704} 705 706type ProfileOverviewParams struct { 707 BaseParams 708 Repos []models.Repo 709 CollaboratingRepos []models.Repo 710 ProfileTimeline *models.ProfileTimeline 711 Card *ProfileCard 712 Active string 713 ShowPunchcard bool 714} 715 716func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 717 params.Active = "overview" 718 return p.executeProfile("user/overview", w, params) 719} 720 721type ProfileReposParams struct { 722 BaseParams 723 Repos []models.Repo 724 StarStatuses map[string]bool 725 Card *ProfileCard 726 Active string 727 Page pagination.Page 728 RepoCount int 729 FilterQuery string 730} 731 732func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 733 params.Active = "repos" 734 return p.executeProfile("user/repos", w, params) 735} 736 737type ProfileStarredParams struct { 738 BaseParams 739 Repos []models.Repo 740 Card *ProfileCard 741 Page pagination.Page 742 Total int 743 Active string 744} 745 746func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 747 params.Active = "starred" 748 return p.executeProfile("user/starred", w, params) 749} 750 751type ProfileStringsParams struct { 752 BaseParams 753 Strings []models.String 754 Card *ProfileCard 755 Active string 756} 757 758func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 759 params.Active = "strings" 760 return p.executeProfile("user/strings", w, params) 761} 762 763type ProfileVouchesParams struct { 764 BaseParams 765 Vouches []models.Vouch 766 Suggestions []models.VouchSuggestion 767 Card *ProfileCard 768 Page pagination.Page 769 VouchCount int 770 Active string 771 EvidencePulls map[syntax.ATURI]*models.Pull 772 EvidenceIssues map[syntax.ATURI]*models.Issue 773} 774 775func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 776 params.Active = "vouches" 777 return p.executeProfile("user/vouches", w, params) 778} 779 780type FollowCard struct { 781 UserDid string 782 BaseParams 783 FollowStatus models.FollowStatus 784 FollowersCount int64 785 FollowingCount int64 786 Profile *models.Profile 787} 788 789type ProfileFollowersParams struct { 790 BaseParams 791 Followers []FollowCard 792 Card *ProfileCard 793 Active string 794} 795 796func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 797 params.Active = "overview" 798 return p.executeProfile("user/followers", w, params) 799} 800 801type ProfileFollowingParams struct { 802 BaseParams 803 Following []FollowCard 804 Card *ProfileCard 805 Active string 806} 807 808func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 809 params.Active = "overview" 810 return p.executeProfile("user/following", w, params) 811} 812 813type FollowFragmentParams struct { 814 UserDid string 815 FollowStatus models.FollowStatus 816 FollowersCount int64 817} 818 819func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 820 return p.executePlain("user/fragments/follow-oob", w, params) 821} 822 823type ProfilePopoverParams struct { 824 BaseParams 825 UserDid string 826 Profile *models.Profile 827 FollowStatus models.FollowStatus 828 VouchRelationship *models.VouchRelationship 829 Stats ProfilePopoverStats 830} 831 832type ProfilePopoverStats struct { 833 FollowersCount int64 834 FollowingCount int64 835} 836 837func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 838 return p.executePlain("user/fragments/profilePopover", w, params) 839} 840 841type EditBioParams struct { 842 BaseParams 843 Profile *models.Profile 844 AlsoKnownAs []string 845} 846 847func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 848 return p.executePlain("user/fragments/editBio", w, params) 849} 850 851type EditPinsParams struct { 852 BaseParams 853 Profile *models.Profile 854 AllRepos []PinnedRepo 855} 856 857type PinnedRepo struct { 858 IsPinned bool 859 models.Repo 860} 861 862func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 863 return p.executePlain("user/fragments/editPins", w, params) 864} 865 866type StarBtnFragmentParams struct { 867 IsStarred bool 868 SubjectAt syntax.ATURI 869 StarCount int 870 RepoName string 871 HxSwapOob bool 872} 873 874func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 875 params.HxSwapOob = true 876 return p.executePlain("fragments/starBtn", w, params) 877} 878 879type RepoIndexParams struct { 880 BaseParams 881 RepoInfo repoinfo.RepoInfo 882 Active string 883 TagMap map[string][]string 884 CommitsTrunc []types.Commit 885 TagsTrunc []*types.TagReference 886 BranchesTrunc []types.Branch 887 // ForkInfo *types.ForkInfo 888 HTMLReadme template.HTML 889 Raw bool 890 EmailToDid map[string]string 891 VerifiedCommits commitverify.VerifiedCommits 892 Languages []types.RepoLanguageDetails 893 Pipelines map[string]models.Pipeline 894 NeedsKnotUpgrade bool 895 KnotUnreachable bool 896 types.RepoIndexResponse 897} 898 899func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 900 params.Active = "overview" 901 if params.IsEmpty { 902 return p.executeRepo("repo/empty", w, params) 903 } 904 905 if params.NeedsKnotUpgrade { 906 return p.executeRepo("repo/needsUpgrade", w, params) 907 } 908 909 if params.KnotUnreachable { 910 return p.executeRepo("repo/knotUnreachable", w, params) 911 } 912 913 rctx := p.rctx.Clone() 914 rctx.RepoInfo = params.RepoInfo 915 rctx.RepoInfo.Ref = params.Ref 916 rctx.RendererType = markup.RendererTypeRepoMarkdown 917 918 if params.ReadmeFileName != "" { 919 switch markup.GetFormat(params.ReadmeFileName) { 920 case markup.FormatMarkdown: 921 params.Raw = false 922 htmlString := rctx.RenderMarkdown(params.Readme) 923 sanitized := sanitizer.SanitizeDefault(htmlString) 924 params.HTMLReadme = template.HTML(sanitized) 925 default: 926 params.Raw = true 927 } 928 } 929 930 return p.executeRepo("repo/index", w, params) 931} 932 933type RepoSearchParams struct { 934 BaseParams 935 RepoInfo repoinfo.RepoInfo 936 Active string 937 FilterQuery string 938} 939 940func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 941 params.Active = "overview" 942 return p.executeRepo("repo/search", w, params) 943} 944 945type RepoSearchResultsFragmentParams struct { 946 Results []SearchResult 947 ErrorMsg string 948} 949 950func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 951 return p.executePlain("repo/fragments/searchResults", w, params) 952} 953 954type RepoLogParams struct { 955 BaseParams 956 RepoInfo repoinfo.RepoInfo 957 TagMap map[string][]string 958 Active string 959 EmailToDid map[string]string 960 VerifiedCommits commitverify.VerifiedCommits 961 Pipelines map[string]models.Pipeline 962 963 types.RepoLogResponse 964} 965 966func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 967 params.Active = "overview" 968 return p.executeRepo("repo/log", w, params) 969} 970 971type RepoCommitParams struct { 972 BaseParams 973 RepoInfo repoinfo.RepoInfo 974 Active string 975 EmailToDid map[string]string 976 Pipeline *models.Pipeline 977 DiffOpts types.DiffOpts 978 979 // singular because it's always going to be just one 980 VerifiedCommit commitverify.VerifiedCommits 981 982 types.RepoCommitResponse 983} 984 985func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 986 params.Active = "overview" 987 return p.executeRepo("repo/commit", w, params) 988} 989 990type RepoTreeParams struct { 991 BaseParams 992 RepoInfo repoinfo.RepoInfo 993 Active string 994 BreadCrumbs [][]string 995 Path string 996 Raw bool 997 HTMLReadme template.HTML 998 EmailToDid map[string]string 999 LastCommitInfo *types.LastCommitInfo 1000 Ref string 1001 Parent string 1002 DotDot string 1003 Files []types.NiceTree 1004 ReadmeFileName string 1005 Readme string 1006} 1007 1008type RepoTreeStats struct { 1009 NumFolders uint64 1010 NumFiles uint64 1011} 1012 1013func (r RepoTreeParams) TreeStats() RepoTreeStats { 1014 numFolders, numFiles := 0, 0 1015 for _, f := range r.Files { 1016 if !f.IsFile() { 1017 numFolders += 1 1018 } else if f.IsFile() { 1019 numFiles += 1 1020 } 1021 } 1022 1023 return RepoTreeStats{ 1024 NumFolders: uint64(numFolders), 1025 NumFiles: uint64(numFiles), 1026 } 1027} 1028 1029func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 1030 params.Active = "overview" 1031 1032 rctx := p.rctx.Clone() 1033 rctx.RepoInfo = params.RepoInfo 1034 rctx.RepoInfo.Ref = params.Ref 1035 rctx.RendererType = markup.RendererTypeRepoMarkdown 1036 1037 if params.ReadmeFileName != "" { 1038 switch markup.GetFormat(params.ReadmeFileName) { 1039 case markup.FormatMarkdown: 1040 params.Raw = false 1041 htmlString := rctx.RenderMarkdown(params.Readme) 1042 sanitized := sanitizer.SanitizeDefault(htmlString) 1043 params.HTMLReadme = template.HTML(sanitized) 1044 default: 1045 params.Raw = true 1046 } 1047 } 1048 1049 return p.executeRepo("repo/tree", w, params) 1050} 1051 1052type RepoBranchesParams struct { 1053 BaseParams 1054 RepoInfo repoinfo.RepoInfo 1055 Active string 1056 types.RepoBranchesResponse 1057} 1058 1059func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 1060 params.Active = "overview" 1061 return p.executeRepo("repo/branches", w, params) 1062} 1063 1064type RepoTagsParams struct { 1065 BaseParams 1066 RepoInfo repoinfo.RepoInfo 1067 Active string 1068 types.RepoTagsResponse 1069 ArtifactMap map[plumbing.Hash][]models.Artifact 1070 DanglingArtifacts []models.Artifact 1071} 1072 1073func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1074 params.Active = "overview" 1075 return p.executeRepo("repo/tags", w, params) 1076} 1077 1078type RepoTagParams struct { 1079 BaseParams 1080 RepoInfo repoinfo.RepoInfo 1081 Active string 1082 types.RepoTagResponse 1083 ArtifactMap map[plumbing.Hash][]models.Artifact 1084 DanglingArtifacts []models.Artifact 1085} 1086 1087func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1088 params.Active = "overview" 1089 return p.executeRepo("repo/tag", w, params) 1090} 1091 1092type RepoArtifactParams struct { 1093 BaseParams 1094 RepoInfo repoinfo.RepoInfo 1095 Artifact models.Artifact 1096} 1097 1098func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1099 return p.executePlain("repo/fragments/artifact", w, params) 1100} 1101 1102type RepoBlobParams struct { 1103 BaseParams 1104 RepoInfo repoinfo.RepoInfo 1105 Active string // always "overview" 1106 BreadCrumbs [][]string 1107 BlobView models.BlobView // TODO: expose this struct 1108 ShowRendered bool 1109 EmailToDid map[string]string 1110 LastCommitInfo *types.LastCommitInfo 1111 Ref string 1112 Path string 1113} 1114 1115func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1116 params.Active = "overview" 1117 return p.executeRepo("repo/blob", w, params) 1118} 1119 1120type Collaborator struct { 1121 Did string 1122 Role string 1123} 1124 1125type RepoSettingsParams struct { 1126 BaseParams 1127 RepoInfo repoinfo.RepoInfo 1128 Collaborators []Collaborator 1129 Active string 1130 Branches []types.Branch 1131 Spindles []string 1132 CurrentSpindle string 1133 Secrets []*tangled.RepoListSecrets_Secret 1134 1135 // TODO: use repoinfo.roles 1136 IsCollaboratorInviteAllowed bool 1137} 1138 1139func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1140 params.Active = "settings" 1141 return p.executeRepo("repo/settings", w, params) 1142} 1143 1144type RepoGeneralSettingsParams struct { 1145 BaseParams 1146 RepoInfo repoinfo.RepoInfo 1147 Labels []models.LabelDefinition 1148 DefaultLabels []models.LabelDefinition 1149 SubscribedLabels map[string]struct{} 1150 ShouldSubscribeAll bool 1151 Active string 1152 Tab string 1153 Branches []types.Branch 1154} 1155 1156func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1157 params.Active = "settings" 1158 params.Tab = "general" 1159 return p.executeRepo("repo/settings/general", w, params) 1160} 1161 1162type RepoAccessSettingsParams struct { 1163 BaseParams 1164 RepoInfo repoinfo.RepoInfo 1165 Active string 1166 Tab string 1167 Collaborators []Collaborator 1168 CanRemoveCollaborator bool 1169} 1170 1171func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1172 params.Active = "settings" 1173 params.Tab = "access" 1174 return p.executeRepo("repo/settings/access", w, params) 1175} 1176 1177type RepoPipelineSettingsParams struct { 1178 BaseParams 1179 RepoInfo repoinfo.RepoInfo 1180 Active string 1181 Tab string 1182 Spindles []string 1183 CurrentSpindle string 1184 Secrets []map[string]any 1185} 1186 1187func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1188 params.Active = "settings" 1189 params.Tab = "pipelines" 1190 return p.executeRepo("repo/settings/pipelines", w, params) 1191} 1192 1193type RepoWebhooksSettingsParams struct { 1194 BaseParams 1195 RepoInfo repoinfo.RepoInfo 1196 Active string 1197 Tab string 1198 Webhooks []models.Webhook 1199 WebhookDeliveries map[int64][]models.WebhookDelivery 1200} 1201 1202func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1203 params.Active = "settings" 1204 params.Tab = "hooks" 1205 return p.executeRepo("repo/settings/hooks", w, params) 1206} 1207 1208type WebhookDeliveriesListParams struct { 1209 BaseParams 1210 RepoInfo repoinfo.RepoInfo 1211 Webhook *models.Webhook 1212 Deliveries []models.WebhookDelivery 1213} 1214 1215func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1216 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1217 if err != nil { 1218 return err 1219 } 1220 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1221} 1222 1223type RepoSiteSettingsParams struct { 1224 BaseParams 1225 RepoInfo repoinfo.RepoInfo 1226 Active string 1227 Tab string 1228 Branches []types.Branch 1229 SiteConfig *models.RepoSite 1230 OwnerClaim *models.DomainClaim 1231 Deploys []models.SiteDeploy 1232 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1233} 1234 1235func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1236 params.Active = "settings" 1237 params.Tab = "sites" 1238 return p.executeRepo("repo/settings/sites", w, params) 1239} 1240 1241type RepoIssuesParams struct { 1242 BaseParams 1243 RepoInfo repoinfo.RepoInfo 1244 Active string 1245 Issues []models.Issue 1246 IssueCount int 1247 LabelDefs map[string]*models.LabelDefinition 1248 Page pagination.Page 1249 FilterState string 1250 FilterQuery string 1251 BaseFilterQuery string 1252 VouchRelationships map[syntax.DID]*models.VouchRelationship 1253} 1254 1255func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1256 params.Active = "issues" 1257 return p.executeRepo("repo/issues/issues", w, params) 1258} 1259 1260type RepoSingleIssueParams struct { 1261 BaseParams 1262 RepoInfo repoinfo.RepoInfo 1263 Active string 1264 Issue *models.Issue 1265 CommentList []models.CommentListItem 1266 Backlinks []models.RichReferenceLink 1267 LabelDefs map[string]*models.LabelDefinition 1268 1269 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1270 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1271 VouchRelationships map[syntax.DID]*models.VouchRelationship 1272} 1273 1274func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1275 params.Active = "issues" 1276 return p.executeRepo("repo/issues/issue", w, params) 1277} 1278 1279type EditIssueParams struct { 1280 BaseParams 1281 RepoInfo repoinfo.RepoInfo 1282 Issue *models.Issue 1283 Action string 1284} 1285 1286func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1287 params.Action = "edit" 1288 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1289} 1290 1291type ThreadReactionFragmentParams struct { 1292 Kind models.ReactionKind 1293 Count int 1294 Users []string 1295 IsReacted bool 1296 CommentRkey string 1297 SubjectUri string 1298} 1299 1300func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1301 return p.executePlain("repo/fragments/reaction", w, params) 1302} 1303 1304type RepoNewIssueParams struct { 1305 BaseParams 1306 RepoInfo repoinfo.RepoInfo 1307 Issue *models.Issue // existing issue if any -- passed when editing 1308 Active string 1309 Action string 1310} 1311 1312func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1313 params.Active = "issues" 1314 params.Action = "create" 1315 return p.executeRepo("repo/issues/new", w, params) 1316} 1317 1318type StackedDiff struct { 1319 Diff *types.NiceDiff 1320 Opts types.DiffOpts 1321} 1322 1323type RepoNewPullParams struct { 1324 BaseParams 1325 RepoInfo repoinfo.RepoInfo 1326 Branches []types.Branch 1327 SourceBranches []types.Branch 1328 ForkBranches []types.Branch 1329 Forks []models.Repo 1330 Source Source 1331 SourceBranch string 1332 TargetBranch string 1333 Fork string 1334 Patch string 1335 Title string 1336 Body string 1337 TitleDirty bool 1338 BodyDirty bool 1339 IsStacked bool 1340 Comparison *types.RepoFormatPatchResponse 1341 Diff *types.NiceDiff 1342 DiffOpts types.DiffOpts 1343 StackedDiffs []StackedDiff 1344 MergeCheck *types.MergeCheckResponse 1345 StackTitles map[string]string 1346 StackBodies map[string]string 1347 PrefillError string 1348 Active string 1349 LabelDefs map[string]*models.LabelDefinition 1350 LabelState models.LabelState 1351 StackLabelStates map[string]models.LabelState 1352} 1353 1354func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1355 params.Active = "pulls" 1356 return p.executeRepo("repo/pulls/new", w, params) 1357} 1358 1359func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1360 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1361} 1362 1363func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1364 return p.executePlain("fragments/markdownPreview", w, body) 1365} 1366 1367type RepoPullsParams struct { 1368 BaseParams 1369 RepoInfo repoinfo.RepoInfo 1370 Pulls []*models.Pull 1371 Active string 1372 FilterState string 1373 FilterQuery string 1374 BaseFilterQuery string 1375 Stacks []models.Stack 1376 Pipelines map[string]models.Pipeline 1377 LabelDefs map[string]*models.LabelDefinition 1378 Page pagination.Page 1379 PullCount int 1380 VouchRelationships map[syntax.DID]*models.VouchRelationship 1381} 1382 1383func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1384 params.Active = "pulls" 1385 return p.executeRepo("repo/pulls/pulls", w, params) 1386} 1387 1388type ResubmitResult uint64 1389 1390const ( 1391 ShouldResubmit ResubmitResult = iota 1392 ShouldNotResubmit 1393 Unknown 1394) 1395 1396func (r ResubmitResult) Yes() bool { 1397 return r == ShouldResubmit 1398} 1399func (r ResubmitResult) No() bool { 1400 return r == ShouldNotResubmit 1401} 1402func (r ResubmitResult) Unknown() bool { 1403 return r == Unknown 1404} 1405 1406type RepoSinglePullParams struct { 1407 BaseParams 1408 RepoInfo repoinfo.RepoInfo 1409 Active string 1410 Pull *models.Pull 1411 Stack models.Stack 1412 Backlinks []models.RichReferenceLink 1413 BranchDeleteStatus *models.BranchDeleteStatus 1414 MergeCheck types.MergeCheckResponse 1415 ResubmitCheck ResubmitResult 1416 Pipelines map[string]models.Pipeline 1417 Diff types.DiffRenderer 1418 DiffOpts types.DiffOpts 1419 ActiveRound int 1420 IsInterdiff bool 1421 1422 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1423 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1424 1425 LabelDefs map[string]*models.LabelDefinition 1426 VouchRelationships map[syntax.DID]*models.VouchRelationship 1427 VouchSkips map[syntax.DID]bool 1428} 1429 1430func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1431 params.Active = "pulls" 1432 return p.executeRepo("repo/pulls/pull", w, params) 1433} 1434 1435type PullResubmitParams struct { 1436 BaseParams 1437 RepoInfo repoinfo.RepoInfo 1438 Pull *models.Pull 1439 SubmissionId int 1440} 1441 1442func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1443 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1444} 1445 1446type PullActionsParams struct { 1447 BaseParams 1448 RepoInfo repoinfo.RepoInfo 1449 Pull *models.Pull 1450 RoundNumber int 1451 MergeCheck types.MergeCheckResponse 1452 ResubmitCheck ResubmitResult 1453 BranchDeleteStatus *models.BranchDeleteStatus 1454 Stack models.Stack 1455 1456 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1457 // that fetches the real, checked fragment 1458 Loading bool 1459} 1460 1461func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1462 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1463} 1464 1465type PullNewCommentParams struct { 1466 BaseParams 1467 RepoInfo repoinfo.RepoInfo 1468 Pull *models.Pull 1469 RoundNumber int 1470} 1471 1472func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1473 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1474} 1475 1476type RepoCompareParams struct { 1477 BaseParams 1478 RepoInfo repoinfo.RepoInfo 1479 Forks []models.Repo 1480 Branches []types.Branch 1481 Tags []*types.TagReference 1482 Base string 1483 Head string 1484 Diff *types.NiceDiff 1485 DiffOpts types.DiffOpts 1486 1487 Active string 1488} 1489 1490func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1491 params.Active = "overview" 1492 return p.executeRepo("repo/compare/compare", w, params) 1493} 1494 1495type RepoCompareNewParams struct { 1496 BaseParams 1497 RepoInfo repoinfo.RepoInfo 1498 Forks []models.Repo 1499 Branches []types.Branch 1500 Tags []*types.TagReference 1501 Base string 1502 Head string 1503 1504 Active string 1505} 1506 1507func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1508 params.Active = "overview" 1509 return p.executeRepo("repo/compare/new", w, params) 1510} 1511 1512type RepoCompareAllowPullParams struct { 1513 BaseParams 1514 RepoInfo repoinfo.RepoInfo 1515 Base string 1516 Head string 1517} 1518 1519func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1520 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1521} 1522 1523type RepoCompareDiffFragmentParams struct { 1524 Diff types.NiceDiff 1525 DiffOpts types.DiffOpts 1526} 1527 1528func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1529 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1530} 1531 1532type LabelPanelParams struct { 1533 BaseParams 1534 RepoInfo repoinfo.RepoInfo 1535 Defs map[string]*models.LabelDefinition 1536 Subject string 1537 State models.LabelState 1538} 1539 1540func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1541 return p.executePlain("repo/fragments/labelPanel", w, params) 1542} 1543 1544type EditLabelPanelParams struct { 1545 BaseParams 1546 RepoInfo repoinfo.RepoInfo 1547 Defs map[string]*models.LabelDefinition 1548 Subject string 1549 State models.LabelState 1550 Prefix string 1551} 1552 1553func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1554 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1555} 1556 1557type RepoStarsParams struct { 1558 BaseParams 1559 RepoInfo repoinfo.RepoInfo 1560 Active string 1561 Starrers []models.Star 1562 Page pagination.Page 1563 TotalCount int 1564} 1565 1566func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1567 params.Active = "overview" 1568 return p.executeRepo("repo/stars", w, params) 1569} 1570 1571type RepoForksParams struct { 1572 BaseParams 1573 RepoInfo repoinfo.RepoInfo 1574 Active string 1575 Forks []models.Repo 1576 Page pagination.Page 1577 TotalCount int 1578} 1579 1580func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1581 params.Active = "overview" 1582 return p.executeRepo("repo/forks", w, params) 1583} 1584 1585type PipelinesParams struct { 1586 BaseParams 1587 RepoInfo repoinfo.RepoInfo 1588 Pipelines []models.Pipeline 1589 Active string 1590 FilterKind string 1591 Total int64 1592} 1593 1594func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1595 params.Active = "pipelines" 1596 return p.executeRepo("repo/pipelines/pipelines", w, params) 1597} 1598 1599type LogBlockParams struct { 1600 Id int 1601 Name string 1602 Command string 1603 Collapsed bool 1604 StartTime time.Time 1605} 1606 1607func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1608 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1609} 1610 1611type LogBlockEndParams struct { 1612 Id int 1613 StartTime time.Time 1614 EndTime time.Time 1615} 1616 1617func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1618 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1619} 1620 1621type LogLineParams struct { 1622 Id int 1623 Content template.HTML 1624} 1625 1626func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1627 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1628} 1629 1630type WorkflowSymbolOOBParams struct { 1631 Name string 1632 Statuses models.WorkflowStatus 1633} 1634 1635func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1636 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1637} 1638 1639type WorkflowParams struct { 1640 BaseParams 1641 RepoInfo repoinfo.RepoInfo 1642 Pipeline models.Pipeline 1643 Workflow string 1644 LogUrl string 1645 Active string 1646} 1647 1648func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1649 params.Active = "pipelines" 1650 return p.executeRepo("repo/pipelines/workflow", w, params) 1651} 1652 1653type PutStringParams struct { 1654 BaseParams 1655 Action string 1656 1657 // this is supplied in the case of editing an existing string 1658 String models.String 1659} 1660 1661func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1662 return p.execute("strings/put", w, params) 1663} 1664 1665type StringsDashboardParams struct { 1666 BaseParams 1667 Card ProfileCard 1668 Strings []models.String 1669} 1670 1671func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1672 return p.execute("strings/dashboard", w, params) 1673} 1674 1675type StringTimelineParams struct { 1676 BaseParams 1677 Strings []models.String 1678} 1679 1680func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1681 return p.execute("strings/timeline", w, params) 1682} 1683 1684type SingleStringParams struct { 1685 BaseParams 1686 ShowRendered bool 1687 RenderToggle bool 1688 RenderedContents template.HTML 1689 String *models.String 1690 Stats models.StringStats 1691 IsStarred bool 1692 StarCount int 1693 Owner identity.Identity 1694 CommentList []models.CommentListItem 1695 1696 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1697 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1698 VouchRelationships map[syntax.DID]*models.VouchRelationship 1699} 1700 1701func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1702 return p.execute("strings/string", w, params) 1703} 1704 1705type SearchReposParams struct { 1706 BaseParams 1707 FilterType string // "repo" | "code" 1708 Repos []SearchResult 1709 Page pagination.Page 1710 ResultCount int 1711 FilterQuery string 1712 SortParam string 1713 TimeTaken time.Duration 1714 DocCount int64 1715 ErrorMsg string 1716} 1717 1718func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1719 params.FilterType = "repo" 1720 return p.execute("search/search", w, params) 1721} 1722 1723type SearchQuickParams struct { 1724 Repos []models.Repo 1725 Query string 1726 Total int 1727} 1728 1729func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1730 return p.executePlain("search/fragments/quick", w, params) 1731} 1732 1733func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1734 tpl, err := p.parse("search/fragments/quick") 1735 if err != nil { 1736 return err 1737 } 1738 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1739} 1740 1741type SearchResult struct { 1742 RepoDID syntax.DID 1743 Repo *models.Repo 1744 FilePath string 1745 Branches []string 1746 Commit string 1747 Language string 1748 1749 File *CodeSearchResult_File // filename match 1750 Chunks CodeSearchResult_Chunks // content matches 1751} 1752 1753// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1754type CodeSearchResult_Chunk struct { 1755 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1756 MatchCount int // number of match ranges in this chunk 1757} 1758 1759type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1760 1761func (cs CodeSearchResult_Chunks) MatchCount() int { 1762 count := 0 1763 for _, c := range cs { 1764 count += c.MatchCount 1765 } 1766 return count 1767} 1768 1769type CodeSearchResult_File struct { 1770 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1771} 1772 1773type ChunkSpan struct { 1774 Text string 1775 Match bool 1776} 1777 1778type ChunkLine struct { 1779 Num int 1780 Spans []ChunkSpan 1781 Highlight bool 1782} 1783 1784// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1785// line into matched/unmatched spans using ranges. startLine is the 1-based line 1786// number of the first line. 1787func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1788 if startLine < 1 { 1789 startLine = 1 1790 } 1791 // trim a single trailing newline so we don't emit a spurious empty line 1792 content = strings.TrimSuffix(content, "\n") 1793 lines := strings.Split(content, "\n") 1794 out := make([]ChunkLine, len(lines)) 1795 for i, text := range lines { 1796 num := startLine + i 1797 runes := []rune(text) 1798 1799 // collect matched rune intervals [c0,c1) for this line 1800 var intervals [][2]int 1801 for _, rg := range ranges { 1802 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1803 continue 1804 } 1805 c0, c1 := 0, len(runes) 1806 if num == int(rg.Start.LineNumber) { 1807 c0 = int(rg.Start.Column) - 1 1808 } 1809 if num == int(rg.End.LineNumber) { 1810 c1 = int(rg.End.Column) - 1 1811 } 1812 c0 = max(0, min(c0, len(runes))) 1813 c1 = max(0, min(c1, len(runes))) 1814 if c0 < c1 { 1815 intervals = append(intervals, [2]int{c0, c1}) 1816 } 1817 } 1818 intervals = mergeIntervals(intervals) 1819 1820 out[i] = ChunkLine{ 1821 Num: num, 1822 Spans: spanRunes(runes, intervals), 1823 Highlight: len(intervals) > 0, 1824 } 1825 } 1826 return out 1827} 1828 1829// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1830// Filename ranges live on line 1; columns are clamped to rune bounds. 1831func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1832 runes := []rune(name) 1833 var intervals [][2]int 1834 for _, rg := range ranges { 1835 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1836 continue 1837 } 1838 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1839 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1840 if c0 < c1 { 1841 intervals = append(intervals, [2]int{c0, c1}) 1842 } 1843 } 1844 return spanRunes(runes, mergeIntervals(intervals)) 1845} 1846 1847type CodeSearchParams struct { 1848 BaseParams 1849 FilterType string // "code" 1850 FilterQuery string 1851 Results []SearchResult 1852 Page pagination.Page 1853 HasMore bool 1854 ErrorMsg string 1855 1856 MatchCount int 1857 FileCount int 1858 TimeTaken time.Duration 1859} 1860 1861func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1862 params.FilterType = "code" 1863 return p.execute("search/search", w, params) 1864} 1865 1866func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1867 return p.execute("timeline/home", w, params) 1868} 1869 1870type CommentBodyFragmentParams struct { 1871 Comment models.Comment 1872 Reactions map[models.ReactionKind]models.ReactionDisplayData 1873 UserReacted map[models.ReactionKind]bool 1874} 1875 1876func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1877 return p.executePlain("fragments/comment/commentBody", w, params) 1878} 1879 1880type CommentHeaderFragmentParams struct { 1881 Comment models.Comment 1882 Reactions map[models.ReactionKind]models.ReactionDisplayData 1883 UserReacted map[models.ReactionKind]bool 1884 HxSwapOob bool 1885} 1886 1887func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 1888 return p.executePlain("fragments/comment/commentHeader", w, params) 1889} 1890 1891type EditCommentFragmentParams struct { 1892 Comment models.Comment 1893} 1894 1895func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1896 return p.executePlain("fragments/comment/edit", w, params) 1897} 1898 1899type ReplyCommentFragmentParams struct { 1900 BaseParams 1901} 1902 1903func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1904 return p.executePlain("fragments/comment/reply", w, params) 1905} 1906 1907type ReplyPlaceholderFragmentParams struct { 1908 BaseParams 1909} 1910 1911func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1912 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1913} 1914 1915func (p *Pages) Static() http.Handler { 1916 if p.dev { 1917 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1918 } 1919 1920 sub, err := fs.Sub(p.embedFS, "static") 1921 if err != nil { 1922 p.logger.Error("no static dir found? that's crazy", "err", err) 1923 panic(err) 1924 } 1925 // Custom handler to apply Cache-Control headers for font files 1926 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1927} 1928 1929func Cache(h http.Handler) http.Handler { 1930 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1931 path := strings.Split(r.URL.Path, "?")[0] 1932 1933 if strings.HasSuffix(path, ".css") { 1934 // on day for css files 1935 w.Header().Set("Cache-Control", "public, max-age=86400") 1936 } else { 1937 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1938 } 1939 h.ServeHTTP(w, r) 1940 }) 1941} 1942 1943func (p *Pages) CssContentHash() string { 1944 cssFile, err := p.embedFS.Open("static/tw.css") 1945 if err != nil { 1946 slog.Debug("Error opening CSS file", "err", err) 1947 return "" 1948 } 1949 defer cssFile.Close() 1950 1951 hasher := sha256.New() 1952 if _, err := io.Copy(hasher, cssFile); err != nil { 1953 slog.Debug("Error hashing CSS file", "err", err) 1954 return "" 1955 } 1956 1957 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1958} 1959 1960func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1961 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1962} 1963 1964func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1965 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1966} 1967 1968func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1969 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1970} 1971 1972func (p *Pages) Error500(w io.Writer) error { 1973 return p.execute("errors/500", w, nil) 1974} 1975 1976func (p *Pages) Error404(w io.Writer) error { 1977 return p.execute("errors/404", w, nil) 1978} 1979 1980func (p *Pages) ErrorKnot404(w io.Writer) error { 1981 return p.execute("errors/knot404", w, nil) 1982} 1983 1984func (p *Pages) Error503(w io.Writer) error { 1985 return p.execute("errors/503", w, nil) 1986}