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