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