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