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