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