this repo has no description
1package pages 2 3import ( 4 "bytes" 5 "crypto/sha256" 6 "embed" 7 "encoding/hex" 8 "fmt" 9 "html/template" 10 "io" 11 "io/fs" 12 "log/slog" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 "sync" 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview/commitverify" 21 "tangled.sh/tangled.sh/core/appview/config" 22 "tangled.sh/tangled.sh/core/appview/db" 23 "tangled.sh/tangled.sh/core/appview/oauth" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 "tangled.sh/tangled.sh/core/idresolver" 28 "tangled.sh/tangled.sh/core/patchutil" 29 "tangled.sh/tangled.sh/core/types" 30 31 "github.com/alecthomas/chroma/v2" 32 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 33 "github.com/alecthomas/chroma/v2/lexers" 34 "github.com/alecthomas/chroma/v2/styles" 35 "github.com/bluesky-social/indigo/atproto/identity" 36 "github.com/bluesky-social/indigo/atproto/syntax" 37 "github.com/go-git/go-git/v5/plumbing" 38 "github.com/go-git/go-git/v5/plumbing/object" 39) 40 41//go:embed templates/* static 42var Files embed.FS 43 44type Pages struct { 45 mu sync.RWMutex 46 t map[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 t: make(map[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 // Initial load of all templates 78 p.loadAllTemplates() 79 80 return p 81} 82 83func (p *Pages) fragmentPaths() ([]string, error) { 84 var fragmentPaths []string 85 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 86 if err != nil { 87 return err 88 } 89 if d.IsDir() { 90 return nil 91 } 92 if !strings.HasSuffix(path, ".html") { 93 return nil 94 } 95 if !strings.Contains(path, "fragments/") { 96 return nil 97 } 98 fragmentPaths = append(fragmentPaths, path) 99 return nil 100 }) 101 if err != nil { 102 return nil, err 103 } 104 105 return fragmentPaths, nil 106} 107 108func (p *Pages) loadAllTemplates() { 109 if p.dev { 110 p.embedFS = os.DirFS(p.templateDir) 111 } else { 112 p.embedFS = Files 113 } 114 115 l := p.logger.With("handler", "loadAllTemplates") 116 templates := make(map[string]*template.Template) 117 fragmentPaths, err := p.fragmentPaths() 118 if err != nil { 119 l.Error("failed to collect fragments", "err", err) 120 return 121 } 122 123 // parse all fragments together 124 allFragments := template.New("").Funcs(p.funcMap()) 125 for _, f := range fragmentPaths { 126 name := strings.TrimPrefix(f, "templates/") 127 name = strings.TrimSuffix(name, ".html") 128 pf, err := template.New(name).Funcs(p.funcMap()).ParseFS(p.embedFS, f) 129 if err != nil { 130 l.Error("failed to parse fragment", "name", name, "path", f) 131 return 132 } 133 allFragments, err = allFragments.AddParseTree(name, pf.Tree) 134 if err != nil { 135 l.Error("failed to add parse tree", "name", name, "path", f) 136 return 137 } 138 templates[name] = allFragments.Lookup(name) 139 } 140 // Then walk through and setup the rest of the templates 141 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 142 if err != nil { 143 return err 144 } 145 if d.IsDir() { 146 return nil 147 } 148 if !strings.HasSuffix(path, "html") { 149 return nil 150 } 151 // Skip fragments as they've already been loaded 152 if strings.Contains(path, "fragments/") { 153 return nil 154 } 155 // Skip layouts 156 if strings.Contains(path, "layouts/") { 157 return nil 158 } 159 name := strings.TrimPrefix(path, "templates/") 160 name = strings.TrimSuffix(name, ".html") 161 // Add the page template on top of the base 162 allPaths := []string{} 163 allPaths = append(allPaths, "templates/layouts/*.html") 164 allPaths = append(allPaths, fragmentPaths...) 165 allPaths = append(allPaths, path) 166 tmpl, err := template.New(name). 167 Funcs(p.funcMap()). 168 ParseFS(p.embedFS, allPaths...) 169 if err != nil { 170 return fmt.Errorf("setting up template: %w", err) 171 } 172 templates[name] = tmpl 173 l.Debug("loaded all templates") 174 return nil 175 }) 176 if err != nil { 177 l.Error("walking template dir", "err", err) 178 panic(err) 179 } 180 181 l.Info("loaded all templates", "total", len(templates)) 182 p.mu.Lock() 183 defer p.mu.Unlock() 184 p.t = templates 185} 186 187func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 188 // In dev mode, reparse templates from disk before executing 189 if p.dev { 190 p.loadAllTemplates() 191 } 192 193 p.mu.RLock() 194 defer p.mu.RUnlock() 195 tmpl, exists := p.t[templateName] 196 if !exists { 197 return fmt.Errorf("template not found: %s", templateName) 198 } 199 200 if base == "" { 201 return tmpl.Execute(w, params) 202 } else { 203 return tmpl.ExecuteTemplate(w, base, params) 204 } 205} 206 207func (p *Pages) execute(name string, w io.Writer, params any) error { 208 return p.executeOrReload(name, w, "layouts/base", params) 209} 210 211func (p *Pages) executePlain(name string, w io.Writer, params any) error { 212 return p.executeOrReload(name, w, "", params) 213} 214 215func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 216 return p.executeOrReload(name, w, "layouts/repobase", params) 217} 218 219func (p *Pages) Favicon(w io.Writer) error { 220 return p.executePlain("favicon", w, nil) 221} 222 223type LoginParams struct { 224 ReturnUrl string 225} 226 227func (p *Pages) Login(w io.Writer, params LoginParams) error { 228 return p.executePlain("user/login", w, params) 229} 230 231func (p *Pages) Signup(w io.Writer) error { 232 return p.executePlain("user/signup", w, nil) 233} 234 235func (p *Pages) CompleteSignup(w io.Writer) error { 236 return p.executePlain("user/completeSignup", w, nil) 237} 238 239type TermsOfServiceParams struct { 240 LoggedInUser *oauth.User 241} 242 243func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 244 return p.execute("legal/terms", w, params) 245} 246 247type PrivacyPolicyParams struct { 248 LoggedInUser *oauth.User 249} 250 251func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 252 return p.execute("legal/privacy", w, params) 253} 254 255type TimelineParams struct { 256 LoggedInUser *oauth.User 257 Timeline []db.TimelineEvent 258 Repos []db.Repo 259} 260 261func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 262 return p.execute("timeline/timeline", w, params) 263} 264 265type UserProfileSettingsParams struct { 266 LoggedInUser *oauth.User 267 Tabs []map[string]any 268 Tab string 269} 270 271func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 272 return p.execute("user/settings/profile", w, params) 273} 274 275type UserKeysSettingsParams struct { 276 LoggedInUser *oauth.User 277 PubKeys []db.PublicKey 278 Tabs []map[string]any 279 Tab string 280} 281 282func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 283 return p.execute("user/settings/keys", w, params) 284} 285 286type UserEmailsSettingsParams struct { 287 LoggedInUser *oauth.User 288 Emails []db.Email 289 Tabs []map[string]any 290 Tab string 291} 292 293func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 294 return p.execute("user/settings/emails", w, params) 295} 296 297type KnotBannerParams struct { 298 Registrations []db.Registration 299} 300 301func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { 302 return p.executePlain("knots/fragments/banner", w, params) 303} 304 305type KnotsParams struct { 306 LoggedInUser *oauth.User 307 Registrations []db.Registration 308} 309 310func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 311 return p.execute("knots/index", w, params) 312} 313 314type KnotParams struct { 315 LoggedInUser *oauth.User 316 Registration *db.Registration 317 Members []string 318 Repos map[string][]db.Repo 319 IsOwner bool 320} 321 322func (p *Pages) Knot(w io.Writer, params KnotParams) error { 323 return p.execute("knots/dashboard", w, params) 324} 325 326type KnotListingParams struct { 327 *db.Registration 328} 329 330func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 331 return p.executePlain("knots/fragments/knotListing", w, params) 332} 333 334type SpindlesParams struct { 335 LoggedInUser *oauth.User 336 Spindles []db.Spindle 337} 338 339func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 340 return p.execute("spindles/index", w, params) 341} 342 343type SpindleListingParams struct { 344 db.Spindle 345} 346 347func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 348 return p.executePlain("spindles/fragments/spindleListing", w, params) 349} 350 351type SpindleDashboardParams struct { 352 LoggedInUser *oauth.User 353 Spindle db.Spindle 354 Members []string 355 Repos map[string][]db.Repo 356} 357 358func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 359 return p.execute("spindles/dashboard", w, params) 360} 361 362type NewRepoParams struct { 363 LoggedInUser *oauth.User 364 Knots []string 365} 366 367func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 368 return p.execute("repo/new", w, params) 369} 370 371type ForkRepoParams struct { 372 LoggedInUser *oauth.User 373 Knots []string 374 RepoInfo repoinfo.RepoInfo 375} 376 377func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 378 return p.execute("repo/fork", w, params) 379} 380 381type ProfileHomePageParams struct { 382 LoggedInUser *oauth.User 383 Repos []db.Repo 384 CollaboratingRepos []db.Repo 385 ProfileTimeline *db.ProfileTimeline 386 Card ProfileCard 387 Punchcard db.Punchcard 388} 389 390type ProfileCard struct { 391 UserDid string 392 UserHandle string 393 FollowStatus db.FollowStatus 394 FollowersCount int 395 FollowingCount int 396 397 Profile *db.Profile 398} 399 400func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 401 return p.execute("user/profile", w, params) 402} 403 404type ReposPageParams struct { 405 LoggedInUser *oauth.User 406 Repos []db.Repo 407 Card ProfileCard 408} 409 410func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 411 return p.execute("user/repos", w, params) 412} 413 414type FollowCard struct { 415 UserDid string 416 FollowStatus db.FollowStatus 417 FollowersCount int 418 FollowingCount int 419 Profile *db.Profile 420} 421 422type FollowersPageParams struct { 423 LoggedInUser *oauth.User 424 Followers []FollowCard 425 Card ProfileCard 426} 427 428func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 429 return p.execute("user/followers", w, params) 430} 431 432type FollowingPageParams struct { 433 LoggedInUser *oauth.User 434 Following []FollowCard 435 Card ProfileCard 436} 437 438func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 439 return p.execute("user/following", w, params) 440} 441 442type FollowFragmentParams struct { 443 UserDid string 444 FollowStatus db.FollowStatus 445} 446 447func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 448 return p.executePlain("user/fragments/follow", w, params) 449} 450 451type EditBioParams struct { 452 LoggedInUser *oauth.User 453 Profile *db.Profile 454} 455 456func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 457 return p.executePlain("user/fragments/editBio", w, params) 458} 459 460type EditPinsParams struct { 461 LoggedInUser *oauth.User 462 Profile *db.Profile 463 AllRepos []PinnedRepo 464} 465 466type PinnedRepo struct { 467 IsPinned bool 468 db.Repo 469} 470 471func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 472 return p.executePlain("user/fragments/editPins", w, params) 473} 474 475type RepoStarFragmentParams struct { 476 IsStarred bool 477 RepoAt syntax.ATURI 478 Stats db.RepoStats 479} 480 481func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error { 482 return p.executePlain("repo/fragments/repoStar", w, params) 483} 484 485type RepoDescriptionParams struct { 486 RepoInfo repoinfo.RepoInfo 487} 488 489func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 490 return p.executePlain("repo/fragments/editRepoDescription", w, params) 491} 492 493func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 494 return p.executePlain("repo/fragments/repoDescription", w, params) 495} 496 497type RepoIndexParams struct { 498 LoggedInUser *oauth.User 499 RepoInfo repoinfo.RepoInfo 500 Active string 501 TagMap map[string][]string 502 CommitsTrunc []*object.Commit 503 TagsTrunc []*types.TagReference 504 BranchesTrunc []types.Branch 505 // ForkInfo *types.ForkInfo 506 HTMLReadme template.HTML 507 Raw bool 508 EmailToDidOrHandle map[string]string 509 VerifiedCommits commitverify.VerifiedCommits 510 Languages []types.RepoLanguageDetails 511 Pipelines map[string]db.Pipeline 512 types.RepoIndexResponse 513} 514 515func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 516 params.Active = "overview" 517 if params.IsEmpty { 518 return p.executeRepo("repo/empty", w, params) 519 } 520 521 p.rctx.RepoInfo = params.RepoInfo 522 p.rctx.RepoInfo.Ref = params.Ref 523 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 524 525 if params.ReadmeFileName != "" { 526 ext := filepath.Ext(params.ReadmeFileName) 527 switch ext { 528 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 529 params.Raw = false 530 htmlString := p.rctx.RenderMarkdown(params.Readme) 531 sanitized := p.rctx.SanitizeDefault(htmlString) 532 params.HTMLReadme = template.HTML(sanitized) 533 default: 534 params.Raw = true 535 } 536 } 537 538 return p.executeRepo("repo/index", w, params) 539} 540 541type RepoLogParams struct { 542 LoggedInUser *oauth.User 543 RepoInfo repoinfo.RepoInfo 544 TagMap map[string][]string 545 types.RepoLogResponse 546 Active string 547 EmailToDidOrHandle map[string]string 548 VerifiedCommits commitverify.VerifiedCommits 549 Pipelines map[string]db.Pipeline 550} 551 552func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 553 params.Active = "overview" 554 return p.executeRepo("repo/log", w, params) 555} 556 557type RepoCommitParams struct { 558 LoggedInUser *oauth.User 559 RepoInfo repoinfo.RepoInfo 560 Active string 561 EmailToDidOrHandle map[string]string 562 Pipeline *db.Pipeline 563 DiffOpts types.DiffOpts 564 565 // singular because it's always going to be just one 566 VerifiedCommit commitverify.VerifiedCommits 567 568 types.RepoCommitResponse 569} 570 571func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 572 params.Active = "overview" 573 return p.executeRepo("repo/commit", w, params) 574} 575 576type RepoTreeParams struct { 577 LoggedInUser *oauth.User 578 RepoInfo repoinfo.RepoInfo 579 Active string 580 BreadCrumbs [][]string 581 TreePath string 582 types.RepoTreeResponse 583} 584 585type RepoTreeStats struct { 586 NumFolders uint64 587 NumFiles uint64 588} 589 590func (r RepoTreeParams) TreeStats() RepoTreeStats { 591 numFolders, numFiles := 0, 0 592 for _, f := range r.Files { 593 if !f.IsFile { 594 numFolders += 1 595 } else if f.IsFile { 596 numFiles += 1 597 } 598 } 599 600 return RepoTreeStats{ 601 NumFolders: uint64(numFolders), 602 NumFiles: uint64(numFiles), 603 } 604} 605 606func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 607 params.Active = "overview" 608 return p.execute("repo/tree", w, params) 609} 610 611type RepoBranchesParams struct { 612 LoggedInUser *oauth.User 613 RepoInfo repoinfo.RepoInfo 614 Active string 615 types.RepoBranchesResponse 616} 617 618func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 619 params.Active = "overview" 620 return p.executeRepo("repo/branches", w, params) 621} 622 623type RepoTagsParams struct { 624 LoggedInUser *oauth.User 625 RepoInfo repoinfo.RepoInfo 626 Active string 627 types.RepoTagsResponse 628 ArtifactMap map[plumbing.Hash][]db.Artifact 629 DanglingArtifacts []db.Artifact 630} 631 632func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 633 params.Active = "overview" 634 return p.executeRepo("repo/tags", w, params) 635} 636 637type RepoArtifactParams struct { 638 LoggedInUser *oauth.User 639 RepoInfo repoinfo.RepoInfo 640 Artifact db.Artifact 641} 642 643func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 644 return p.executePlain("repo/fragments/artifact", w, params) 645} 646 647type RepoBlobParams struct { 648 LoggedInUser *oauth.User 649 RepoInfo repoinfo.RepoInfo 650 Active string 651 Unsupported bool 652 IsImage bool 653 IsVideo bool 654 ContentSrc string 655 BreadCrumbs [][]string 656 ShowRendered bool 657 RenderToggle bool 658 RenderedContents template.HTML 659 types.RepoBlobResponse 660} 661 662func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 663 var style *chroma.Style = styles.Get("catpuccin-latte") 664 665 if params.ShowRendered { 666 switch markup.GetFormat(params.Path) { 667 case markup.FormatMarkdown: 668 p.rctx.RepoInfo = params.RepoInfo 669 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 670 htmlString := p.rctx.RenderMarkdown(params.Contents) 671 sanitized := p.rctx.SanitizeDefault(htmlString) 672 params.RenderedContents = template.HTML(sanitized) 673 } 674 } 675 676 c := params.Contents 677 formatter := chromahtml.New( 678 chromahtml.InlineCode(false), 679 chromahtml.WithLineNumbers(true), 680 chromahtml.WithLinkableLineNumbers(true, "L"), 681 chromahtml.Standalone(false), 682 chromahtml.WithClasses(true), 683 ) 684 685 lexer := lexers.Get(filepath.Base(params.Path)) 686 if lexer == nil { 687 lexer = lexers.Fallback 688 } 689 690 iterator, err := lexer.Tokenise(nil, c) 691 if err != nil { 692 return fmt.Errorf("chroma tokenize: %w", err) 693 } 694 695 var code bytes.Buffer 696 err = formatter.Format(&code, style, iterator) 697 if err != nil { 698 return fmt.Errorf("chroma format: %w", err) 699 } 700 701 params.Contents = code.String() 702 params.Active = "overview" 703 return p.executeRepo("repo/blob", w, params) 704} 705 706type Collaborator struct { 707 Did string 708 Handle string 709 Role string 710} 711 712type RepoSettingsParams struct { 713 LoggedInUser *oauth.User 714 RepoInfo repoinfo.RepoInfo 715 Collaborators []Collaborator 716 Active string 717 Branches []types.Branch 718 Spindles []string 719 CurrentSpindle string 720 Secrets []*tangled.RepoListSecrets_Secret 721 722 // TODO: use repoinfo.roles 723 IsCollaboratorInviteAllowed bool 724} 725 726func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 727 params.Active = "settings" 728 return p.executeRepo("repo/settings", w, params) 729} 730 731type RepoGeneralSettingsParams struct { 732 LoggedInUser *oauth.User 733 RepoInfo repoinfo.RepoInfo 734 Active string 735 Tabs []map[string]any 736 Tab string 737 Branches []types.Branch 738} 739 740func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 741 params.Active = "settings" 742 return p.executeRepo("repo/settings/general", w, params) 743} 744 745type RepoAccessSettingsParams struct { 746 LoggedInUser *oauth.User 747 RepoInfo repoinfo.RepoInfo 748 Active string 749 Tabs []map[string]any 750 Tab string 751 Collaborators []Collaborator 752} 753 754func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 755 params.Active = "settings" 756 return p.executeRepo("repo/settings/access", w, params) 757} 758 759type RepoPipelineSettingsParams struct { 760 LoggedInUser *oauth.User 761 RepoInfo repoinfo.RepoInfo 762 Active string 763 Tabs []map[string]any 764 Tab string 765 Spindles []string 766 CurrentSpindle string 767 Secrets []map[string]any 768} 769 770func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 771 params.Active = "settings" 772 return p.executeRepo("repo/settings/pipelines", w, params) 773} 774 775type RepoIssuesParams struct { 776 LoggedInUser *oauth.User 777 RepoInfo repoinfo.RepoInfo 778 Active string 779 Issues []db.Issue 780 Page pagination.Page 781 FilteringByOpen bool 782} 783 784func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 785 params.Active = "issues" 786 return p.executeRepo("repo/issues/issues", w, params) 787} 788 789type RepoSingleIssueParams struct { 790 LoggedInUser *oauth.User 791 RepoInfo repoinfo.RepoInfo 792 Active string 793 Issue *db.Issue 794 Comments []db.Comment 795 IssueOwnerHandle string 796 797 OrderedReactionKinds []db.ReactionKind 798 Reactions map[db.ReactionKind]int 799 UserReacted map[db.ReactionKind]bool 800 801 State string 802} 803 804type ThreadReactionFragmentParams struct { 805 ThreadAt syntax.ATURI 806 Kind db.ReactionKind 807 Count int 808 IsReacted bool 809} 810 811func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 812 return p.executePlain("repo/fragments/reaction", w, params) 813} 814 815func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 816 params.Active = "issues" 817 if params.Issue.Open { 818 params.State = "open" 819 } else { 820 params.State = "closed" 821 } 822 return p.execute("repo/issues/issue", w, params) 823} 824 825type RepoNewIssueParams struct { 826 LoggedInUser *oauth.User 827 RepoInfo repoinfo.RepoInfo 828 Active string 829} 830 831func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 832 params.Active = "issues" 833 return p.executeRepo("repo/issues/new", w, params) 834} 835 836type EditIssueCommentParams struct { 837 LoggedInUser *oauth.User 838 RepoInfo repoinfo.RepoInfo 839 Issue *db.Issue 840 Comment *db.Comment 841} 842 843func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 844 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 845} 846 847type SingleIssueCommentParams struct { 848 LoggedInUser *oauth.User 849 RepoInfo repoinfo.RepoInfo 850 Issue *db.Issue 851 Comment *db.Comment 852} 853 854func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 855 return p.executePlain("repo/issues/fragments/issueComment", w, params) 856} 857 858type RepoNewPullParams struct { 859 LoggedInUser *oauth.User 860 RepoInfo repoinfo.RepoInfo 861 Branches []types.Branch 862 Strategy string 863 SourceBranch string 864 TargetBranch string 865 Title string 866 Body string 867 Active string 868} 869 870func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 871 params.Active = "pulls" 872 return p.executeRepo("repo/pulls/new", w, params) 873} 874 875type RepoPullsParams struct { 876 LoggedInUser *oauth.User 877 RepoInfo repoinfo.RepoInfo 878 Pulls []*db.Pull 879 Active string 880 FilteringBy db.PullState 881 Stacks map[string]db.Stack 882 Pipelines map[string]db.Pipeline 883} 884 885func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 886 params.Active = "pulls" 887 return p.executeRepo("repo/pulls/pulls", w, params) 888} 889 890type ResubmitResult uint64 891 892const ( 893 ShouldResubmit ResubmitResult = iota 894 ShouldNotResubmit 895 Unknown 896) 897 898func (r ResubmitResult) Yes() bool { 899 return r == ShouldResubmit 900} 901func (r ResubmitResult) No() bool { 902 return r == ShouldNotResubmit 903} 904func (r ResubmitResult) Unknown() bool { 905 return r == Unknown 906} 907 908type RepoSinglePullParams struct { 909 LoggedInUser *oauth.User 910 RepoInfo repoinfo.RepoInfo 911 Active string 912 Pull *db.Pull 913 Stack db.Stack 914 AbandonedPulls []*db.Pull 915 MergeCheck types.MergeCheckResponse 916 ResubmitCheck ResubmitResult 917 Pipelines map[string]db.Pipeline 918 919 OrderedReactionKinds []db.ReactionKind 920 Reactions map[db.ReactionKind]int 921 UserReacted map[db.ReactionKind]bool 922} 923 924func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 925 params.Active = "pulls" 926 return p.executeRepo("repo/pulls/pull", w, params) 927} 928 929type RepoPullPatchParams struct { 930 LoggedInUser *oauth.User 931 RepoInfo repoinfo.RepoInfo 932 Pull *db.Pull 933 Stack db.Stack 934 Diff *types.NiceDiff 935 Round int 936 Submission *db.PullSubmission 937 OrderedReactionKinds []db.ReactionKind 938 DiffOpts types.DiffOpts 939} 940 941// this name is a mouthful 942func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 943 return p.execute("repo/pulls/patch", w, params) 944} 945 946type RepoPullInterdiffParams struct { 947 LoggedInUser *oauth.User 948 RepoInfo repoinfo.RepoInfo 949 Pull *db.Pull 950 Round int 951 Interdiff *patchutil.InterdiffResult 952 OrderedReactionKinds []db.ReactionKind 953 DiffOpts types.DiffOpts 954} 955 956// this name is a mouthful 957func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 958 return p.execute("repo/pulls/interdiff", w, params) 959} 960 961type PullPatchUploadParams struct { 962 RepoInfo repoinfo.RepoInfo 963} 964 965func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 966 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 967} 968 969type PullCompareBranchesParams struct { 970 RepoInfo repoinfo.RepoInfo 971 Branches []types.Branch 972 SourceBranch string 973} 974 975func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 976 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 977} 978 979type PullCompareForkParams struct { 980 RepoInfo repoinfo.RepoInfo 981 Forks []db.Repo 982 Selected string 983} 984 985func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 986 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 987} 988 989type PullCompareForkBranchesParams struct { 990 RepoInfo repoinfo.RepoInfo 991 SourceBranches []types.Branch 992 TargetBranches []types.Branch 993} 994 995func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 996 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 997} 998 999type PullResubmitParams struct { 1000 LoggedInUser *oauth.User 1001 RepoInfo repoinfo.RepoInfo 1002 Pull *db.Pull 1003 SubmissionId int 1004} 1005 1006func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1007 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1008} 1009 1010type PullActionsParams struct { 1011 LoggedInUser *oauth.User 1012 RepoInfo repoinfo.RepoInfo 1013 Pull *db.Pull 1014 RoundNumber int 1015 MergeCheck types.MergeCheckResponse 1016 ResubmitCheck ResubmitResult 1017 Stack db.Stack 1018} 1019 1020func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1021 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1022} 1023 1024type PullNewCommentParams struct { 1025 LoggedInUser *oauth.User 1026 RepoInfo repoinfo.RepoInfo 1027 Pull *db.Pull 1028 RoundNumber int 1029} 1030 1031func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1032 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1033} 1034 1035type RepoCompareParams struct { 1036 LoggedInUser *oauth.User 1037 RepoInfo repoinfo.RepoInfo 1038 Forks []db.Repo 1039 Branches []types.Branch 1040 Tags []*types.TagReference 1041 Base string 1042 Head string 1043 Diff *types.NiceDiff 1044 DiffOpts types.DiffOpts 1045 1046 Active string 1047} 1048 1049func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1050 params.Active = "overview" 1051 return p.executeRepo("repo/compare/compare", w, params) 1052} 1053 1054type RepoCompareNewParams struct { 1055 LoggedInUser *oauth.User 1056 RepoInfo repoinfo.RepoInfo 1057 Forks []db.Repo 1058 Branches []types.Branch 1059 Tags []*types.TagReference 1060 Base string 1061 Head string 1062 1063 Active string 1064} 1065 1066func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1067 params.Active = "overview" 1068 return p.executeRepo("repo/compare/new", w, params) 1069} 1070 1071type RepoCompareAllowPullParams struct { 1072 LoggedInUser *oauth.User 1073 RepoInfo repoinfo.RepoInfo 1074 Base string 1075 Head string 1076} 1077 1078func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1079 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1080} 1081 1082type RepoCompareDiffParams struct { 1083 LoggedInUser *oauth.User 1084 RepoInfo repoinfo.RepoInfo 1085 Diff types.NiceDiff 1086} 1087 1088func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1089 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1090} 1091 1092type PipelinesParams struct { 1093 LoggedInUser *oauth.User 1094 RepoInfo repoinfo.RepoInfo 1095 Pipelines []db.Pipeline 1096 Active string 1097} 1098 1099func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1100 params.Active = "pipelines" 1101 return p.executeRepo("repo/pipelines/pipelines", w, params) 1102} 1103 1104type LogBlockParams struct { 1105 Id int 1106 Name string 1107 Command string 1108 Collapsed bool 1109} 1110 1111func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1112 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1113} 1114 1115type LogLineParams struct { 1116 Id int 1117 Content string 1118} 1119 1120func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1121 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1122} 1123 1124type WorkflowParams struct { 1125 LoggedInUser *oauth.User 1126 RepoInfo repoinfo.RepoInfo 1127 Pipeline db.Pipeline 1128 Workflow string 1129 LogUrl string 1130 Active string 1131} 1132 1133func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1134 params.Active = "pipelines" 1135 return p.executeRepo("repo/pipelines/workflow", w, params) 1136} 1137 1138type PutStringParams struct { 1139 LoggedInUser *oauth.User 1140 Action string 1141 1142 // this is supplied in the case of editing an existing string 1143 String db.String 1144} 1145 1146func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1147 return p.execute("strings/put", w, params) 1148} 1149 1150type StringsDashboardParams struct { 1151 LoggedInUser *oauth.User 1152 Card ProfileCard 1153 Strings []db.String 1154} 1155 1156func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1157 return p.execute("strings/dashboard", w, params) 1158} 1159 1160type StringTimelineParams struct { 1161 LoggedInUser *oauth.User 1162 Strings []db.String 1163} 1164 1165func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1166 return p.execute("strings/timeline", w, params) 1167} 1168 1169type SingleStringParams struct { 1170 LoggedInUser *oauth.User 1171 ShowRendered bool 1172 RenderToggle bool 1173 RenderedContents template.HTML 1174 String db.String 1175 Stats db.StringStats 1176 Owner identity.Identity 1177} 1178 1179func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1180 var style *chroma.Style = styles.Get("catpuccin-latte") 1181 1182 if params.ShowRendered { 1183 switch markup.GetFormat(params.String.Filename) { 1184 case markup.FormatMarkdown: 1185 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1186 htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1187 sanitized := p.rctx.SanitizeDefault(htmlString) 1188 params.RenderedContents = template.HTML(sanitized) 1189 } 1190 } 1191 1192 c := params.String.Contents 1193 formatter := chromahtml.New( 1194 chromahtml.InlineCode(false), 1195 chromahtml.WithLineNumbers(true), 1196 chromahtml.WithLinkableLineNumbers(true, "L"), 1197 chromahtml.Standalone(false), 1198 chromahtml.WithClasses(true), 1199 ) 1200 1201 lexer := lexers.Get(filepath.Base(params.String.Filename)) 1202 if lexer == nil { 1203 lexer = lexers.Fallback 1204 } 1205 1206 iterator, err := lexer.Tokenise(nil, c) 1207 if err != nil { 1208 return fmt.Errorf("chroma tokenize: %w", err) 1209 } 1210 1211 var code bytes.Buffer 1212 err = formatter.Format(&code, style, iterator) 1213 if err != nil { 1214 return fmt.Errorf("chroma format: %w", err) 1215 } 1216 1217 params.String.Contents = code.String() 1218 return p.execute("strings/string", w, params) 1219} 1220 1221func (p *Pages) Static() http.Handler { 1222 if p.dev { 1223 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1224 } 1225 1226 sub, err := fs.Sub(Files, "static") 1227 if err != nil { 1228 p.logger.Error("no static dir found? that's crazy", "err", err) 1229 panic(err) 1230 } 1231 // Custom handler to apply Cache-Control headers for font files 1232 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1233} 1234 1235func Cache(h http.Handler) http.Handler { 1236 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1237 path := strings.Split(r.URL.Path, "?")[0] 1238 1239 if strings.HasSuffix(path, ".css") { 1240 // on day for css files 1241 w.Header().Set("Cache-Control", "public, max-age=86400") 1242 } else { 1243 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1244 } 1245 h.ServeHTTP(w, r) 1246 }) 1247} 1248 1249func CssContentHash() string { 1250 cssFile, err := Files.Open("static/tw.css") 1251 if err != nil { 1252 slog.Debug("Error opening CSS file", "err", err) 1253 return "" 1254 } 1255 defer cssFile.Close() 1256 1257 hasher := sha256.New() 1258 if _, err := io.Copy(hasher, cssFile); err != nil { 1259 slog.Debug("Error hashing CSS file", "err", err) 1260 return "" 1261 } 1262 1263 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1264} 1265 1266func (p *Pages) Error500(w io.Writer) error { 1267 return p.execute("errors/500", w, nil) 1268} 1269 1270func (p *Pages) Error404(w io.Writer) error { 1271 return p.execute("errors/404", w, nil) 1272} 1273 1274func (p *Pages) ErrorKnot404(w io.Writer) error { 1275 return p.execute("errors/knot404", w, nil) 1276} 1277 1278func (p *Pages) Error503(w io.Writer) error { 1279 return p.execute("errors/503", w, nil) 1280}