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