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 types.RepoTreeResponse 718} 719 720type RepoTreeStats struct { 721 NumFolders uint64 722 NumFiles uint64 723} 724 725func (r RepoTreeParams) TreeStats() RepoTreeStats { 726 numFolders, numFiles := 0, 0 727 for _, f := range r.Files { 728 if !f.IsFile { 729 numFolders += 1 730 } else if f.IsFile { 731 numFiles += 1 732 } 733 } 734 735 return RepoTreeStats{ 736 NumFolders: uint64(numFolders), 737 NumFiles: uint64(numFiles), 738 } 739} 740 741func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 742 params.Active = "overview" 743 return p.executeRepo("repo/tree", w, params) 744} 745 746type RepoBranchesParams struct { 747 LoggedInUser *oauth.User 748 RepoInfo repoinfo.RepoInfo 749 Active string 750 types.RepoBranchesResponse 751} 752 753func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 754 params.Active = "overview" 755 return p.executeRepo("repo/branches", w, params) 756} 757 758type RepoTagsParams struct { 759 LoggedInUser *oauth.User 760 RepoInfo repoinfo.RepoInfo 761 Active string 762 types.RepoTagsResponse 763 ArtifactMap map[plumbing.Hash][]models.Artifact 764 DanglingArtifacts []models.Artifact 765} 766 767func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 768 params.Active = "overview" 769 return p.executeRepo("repo/tags", w, params) 770} 771 772type RepoArtifactParams struct { 773 LoggedInUser *oauth.User 774 RepoInfo repoinfo.RepoInfo 775 Artifact models.Artifact 776} 777 778func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 779 return p.executePlain("repo/fragments/artifact", w, params) 780} 781 782type RepoBlobParams struct { 783 LoggedInUser *oauth.User 784 RepoInfo repoinfo.RepoInfo 785 Active string 786 Unsupported bool 787 IsImage bool 788 IsVideo bool 789 ContentSrc string 790 BreadCrumbs [][]string 791 ShowRendered bool 792 RenderToggle bool 793 RenderedContents template.HTML 794 *tangled.RepoBlob_Output 795 // Computed fields for template compatibility 796 Contents string 797 Lines int 798 SizeHint uint64 799 IsBinary bool 800} 801 802func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 803 var style *chroma.Style = styles.Get("catpuccin-latte") 804 805 if params.ShowRendered { 806 switch markup.GetFormat(params.Path) { 807 case markup.FormatMarkdown: 808 p.rctx.RepoInfo = params.RepoInfo 809 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 810 htmlString := p.rctx.RenderMarkdown(params.Contents) 811 sanitized := p.rctx.SanitizeDefault(htmlString) 812 params.RenderedContents = template.HTML(sanitized) 813 } 814 } 815 816 c := params.Contents 817 formatter := chromahtml.New( 818 chromahtml.InlineCode(false), 819 chromahtml.WithLineNumbers(true), 820 chromahtml.WithLinkableLineNumbers(true, "L"), 821 chromahtml.Standalone(false), 822 chromahtml.WithClasses(true), 823 ) 824 825 lexer := lexers.Get(filepath.Base(params.Path)) 826 if lexer == nil { 827 lexer = lexers.Fallback 828 } 829 830 iterator, err := lexer.Tokenise(nil, c) 831 if err != nil { 832 return fmt.Errorf("chroma tokenize: %w", err) 833 } 834 835 var code bytes.Buffer 836 err = formatter.Format(&code, style, iterator) 837 if err != nil { 838 return fmt.Errorf("chroma format: %w", err) 839 } 840 841 params.Contents = code.String() 842 params.Active = "overview" 843 return p.executeRepo("repo/blob", w, params) 844} 845 846type Collaborator struct { 847 Did string 848 Handle string 849 Role string 850} 851 852type RepoSettingsParams struct { 853 LoggedInUser *oauth.User 854 RepoInfo repoinfo.RepoInfo 855 Collaborators []Collaborator 856 Active string 857 Branches []types.Branch 858 Spindles []string 859 CurrentSpindle string 860 Secrets []*tangled.RepoListSecrets_Secret 861 862 // TODO: use repoinfo.roles 863 IsCollaboratorInviteAllowed bool 864} 865 866func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 867 params.Active = "settings" 868 return p.executeRepo("repo/settings", w, params) 869} 870 871type RepoGeneralSettingsParams struct { 872 LoggedInUser *oauth.User 873 RepoInfo repoinfo.RepoInfo 874 Labels []models.LabelDefinition 875 DefaultLabels []models.LabelDefinition 876 SubscribedLabels map[string]struct{} 877 ShouldSubscribeAll bool 878 Active string 879 Tabs []map[string]any 880 Tab string 881 Branches []types.Branch 882} 883 884func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 885 params.Active = "settings" 886 return p.executeRepo("repo/settings/general", w, params) 887} 888 889type RepoAccessSettingsParams struct { 890 LoggedInUser *oauth.User 891 RepoInfo repoinfo.RepoInfo 892 Active string 893 Tabs []map[string]any 894 Tab string 895 Collaborators []Collaborator 896} 897 898func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 899 params.Active = "settings" 900 return p.executeRepo("repo/settings/access", w, params) 901} 902 903type RepoPipelineSettingsParams struct { 904 LoggedInUser *oauth.User 905 RepoInfo repoinfo.RepoInfo 906 Active string 907 Tabs []map[string]any 908 Tab string 909 Spindles []string 910 CurrentSpindle string 911 Secrets []map[string]any 912} 913 914func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 915 params.Active = "settings" 916 return p.executeRepo("repo/settings/pipelines", w, params) 917} 918 919type RepoIssuesParams struct { 920 LoggedInUser *oauth.User 921 RepoInfo repoinfo.RepoInfo 922 Active string 923 Issues []models.Issue 924 LabelDefs map[string]*models.LabelDefinition 925 Page pagination.Page 926 FilteringByOpen bool 927} 928 929func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 930 params.Active = "issues" 931 return p.executeRepo("repo/issues/issues", w, params) 932} 933 934type RepoSingleIssueParams struct { 935 LoggedInUser *oauth.User 936 RepoInfo repoinfo.RepoInfo 937 Active string 938 Issue *models.Issue 939 CommentList []models.CommentListItem 940 LabelDefs map[string]*models.LabelDefinition 941 942 OrderedReactionKinds []models.ReactionKind 943 Reactions map[models.ReactionKind]int 944 UserReacted map[models.ReactionKind]bool 945} 946 947func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 948 params.Active = "issues" 949 return p.executeRepo("repo/issues/issue", w, params) 950} 951 952type EditIssueParams struct { 953 LoggedInUser *oauth.User 954 RepoInfo repoinfo.RepoInfo 955 Issue *models.Issue 956 Action string 957} 958 959func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 960 params.Action = "edit" 961 return p.executePlain("repo/issues/fragments/putIssue", w, params) 962} 963 964type ThreadReactionFragmentParams struct { 965 ThreadAt syntax.ATURI 966 Kind models.ReactionKind 967 Count int 968 IsReacted bool 969} 970 971func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 972 return p.executePlain("repo/fragments/reaction", w, params) 973} 974 975type RepoNewIssueParams struct { 976 LoggedInUser *oauth.User 977 RepoInfo repoinfo.RepoInfo 978 Issue *models.Issue // existing issue if any -- passed when editing 979 Active string 980 Action string 981} 982 983func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 984 params.Active = "issues" 985 params.Action = "create" 986 return p.executeRepo("repo/issues/new", w, params) 987} 988 989type EditIssueCommentParams struct { 990 LoggedInUser *oauth.User 991 RepoInfo repoinfo.RepoInfo 992 Issue *models.Issue 993 Comment *models.IssueComment 994} 995 996func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 997 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 998} 999 1000type ReplyIssueCommentPlaceholderParams struct { 1001 LoggedInUser *oauth.User 1002 RepoInfo repoinfo.RepoInfo 1003 Issue *models.Issue 1004 Comment *models.IssueComment 1005} 1006 1007func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1008 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1009} 1010 1011type ReplyIssueCommentParams struct { 1012 LoggedInUser *oauth.User 1013 RepoInfo repoinfo.RepoInfo 1014 Issue *models.Issue 1015 Comment *models.IssueComment 1016} 1017 1018func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1019 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1020} 1021 1022type IssueCommentBodyParams struct { 1023 LoggedInUser *oauth.User 1024 RepoInfo repoinfo.RepoInfo 1025 Issue *models.Issue 1026 Comment *models.IssueComment 1027} 1028 1029func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1030 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1031} 1032 1033type RepoNewPullParams struct { 1034 LoggedInUser *oauth.User 1035 RepoInfo repoinfo.RepoInfo 1036 Branches []types.Branch 1037 Strategy string 1038 SourceBranch string 1039 TargetBranch string 1040 Title string 1041 Body string 1042 Active string 1043} 1044 1045func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1046 params.Active = "pulls" 1047 return p.executeRepo("repo/pulls/new", w, params) 1048} 1049 1050type RepoPullsParams struct { 1051 LoggedInUser *oauth.User 1052 RepoInfo repoinfo.RepoInfo 1053 Pulls []*models.Pull 1054 Active string 1055 FilteringBy models.PullState 1056 Stacks map[string]models.Stack 1057 Pipelines map[string]models.Pipeline 1058} 1059 1060func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1061 params.Active = "pulls" 1062 return p.executeRepo("repo/pulls/pulls", w, params) 1063} 1064 1065type ResubmitResult uint64 1066 1067const ( 1068 ShouldResubmit ResubmitResult = iota 1069 ShouldNotResubmit 1070 Unknown 1071) 1072 1073func (r ResubmitResult) Yes() bool { 1074 return r == ShouldResubmit 1075} 1076func (r ResubmitResult) No() bool { 1077 return r == ShouldNotResubmit 1078} 1079func (r ResubmitResult) Unknown() bool { 1080 return r == Unknown 1081} 1082 1083type RepoSinglePullParams struct { 1084 LoggedInUser *oauth.User 1085 RepoInfo repoinfo.RepoInfo 1086 Active string 1087 Pull *models.Pull 1088 Stack models.Stack 1089 AbandonedPulls []*models.Pull 1090 MergeCheck types.MergeCheckResponse 1091 ResubmitCheck ResubmitResult 1092 Pipelines map[string]models.Pipeline 1093 1094 OrderedReactionKinds []models.ReactionKind 1095 Reactions map[models.ReactionKind]int 1096 UserReacted map[models.ReactionKind]bool 1097} 1098 1099func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1100 params.Active = "pulls" 1101 return p.executeRepo("repo/pulls/pull", w, params) 1102} 1103 1104type RepoPullPatchParams struct { 1105 LoggedInUser *oauth.User 1106 RepoInfo repoinfo.RepoInfo 1107 Pull *models.Pull 1108 Stack models.Stack 1109 Diff *types.NiceDiff 1110 Round int 1111 Submission *models.PullSubmission 1112 OrderedReactionKinds []models.ReactionKind 1113 DiffOpts types.DiffOpts 1114} 1115 1116// this name is a mouthful 1117func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1118 return p.execute("repo/pulls/patch", w, params) 1119} 1120 1121type RepoPullInterdiffParams struct { 1122 LoggedInUser *oauth.User 1123 RepoInfo repoinfo.RepoInfo 1124 Pull *models.Pull 1125 Round int 1126 Interdiff *patchutil.InterdiffResult 1127 OrderedReactionKinds []models.ReactionKind 1128 DiffOpts types.DiffOpts 1129} 1130 1131// this name is a mouthful 1132func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1133 return p.execute("repo/pulls/interdiff", w, params) 1134} 1135 1136type PullPatchUploadParams struct { 1137 RepoInfo repoinfo.RepoInfo 1138} 1139 1140func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1141 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1142} 1143 1144type PullCompareBranchesParams struct { 1145 RepoInfo repoinfo.RepoInfo 1146 Branches []types.Branch 1147 SourceBranch string 1148} 1149 1150func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1151 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1152} 1153 1154type PullCompareForkParams struct { 1155 RepoInfo repoinfo.RepoInfo 1156 Forks []models.Repo 1157 Selected string 1158} 1159 1160func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1161 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1162} 1163 1164type PullCompareForkBranchesParams struct { 1165 RepoInfo repoinfo.RepoInfo 1166 SourceBranches []types.Branch 1167 TargetBranches []types.Branch 1168} 1169 1170func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1171 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1172} 1173 1174type PullResubmitParams struct { 1175 LoggedInUser *oauth.User 1176 RepoInfo repoinfo.RepoInfo 1177 Pull *models.Pull 1178 SubmissionId int 1179} 1180 1181func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1182 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1183} 1184 1185type PullActionsParams struct { 1186 LoggedInUser *oauth.User 1187 RepoInfo repoinfo.RepoInfo 1188 Pull *models.Pull 1189 RoundNumber int 1190 MergeCheck types.MergeCheckResponse 1191 ResubmitCheck ResubmitResult 1192 Stack models.Stack 1193} 1194 1195func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1196 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1197} 1198 1199type PullNewCommentParams struct { 1200 LoggedInUser *oauth.User 1201 RepoInfo repoinfo.RepoInfo 1202 Pull *models.Pull 1203 RoundNumber int 1204} 1205 1206func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1207 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1208} 1209 1210type RepoCompareParams struct { 1211 LoggedInUser *oauth.User 1212 RepoInfo repoinfo.RepoInfo 1213 Forks []models.Repo 1214 Branches []types.Branch 1215 Tags []*types.TagReference 1216 Base string 1217 Head string 1218 Diff *types.NiceDiff 1219 DiffOpts types.DiffOpts 1220 1221 Active string 1222} 1223 1224func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1225 params.Active = "overview" 1226 return p.executeRepo("repo/compare/compare", w, params) 1227} 1228 1229type RepoCompareNewParams struct { 1230 LoggedInUser *oauth.User 1231 RepoInfo repoinfo.RepoInfo 1232 Forks []models.Repo 1233 Branches []types.Branch 1234 Tags []*types.TagReference 1235 Base string 1236 Head string 1237 1238 Active string 1239} 1240 1241func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1242 params.Active = "overview" 1243 return p.executeRepo("repo/compare/new", w, params) 1244} 1245 1246type RepoCompareAllowPullParams struct { 1247 LoggedInUser *oauth.User 1248 RepoInfo repoinfo.RepoInfo 1249 Base string 1250 Head string 1251} 1252 1253func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1254 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1255} 1256 1257type RepoCompareDiffParams struct { 1258 LoggedInUser *oauth.User 1259 RepoInfo repoinfo.RepoInfo 1260 Diff types.NiceDiff 1261} 1262 1263func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1264 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1265} 1266 1267type LabelPanelParams struct { 1268 LoggedInUser *oauth.User 1269 RepoInfo repoinfo.RepoInfo 1270 Defs map[string]*models.LabelDefinition 1271 Subject string 1272 State models.LabelState 1273} 1274 1275func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1276 return p.executePlain("repo/fragments/labelPanel", w, params) 1277} 1278 1279type EditLabelPanelParams struct { 1280 LoggedInUser *oauth.User 1281 RepoInfo repoinfo.RepoInfo 1282 Defs map[string]*models.LabelDefinition 1283 Subject string 1284 State models.LabelState 1285} 1286 1287func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1288 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1289} 1290 1291type PipelinesParams struct { 1292 LoggedInUser *oauth.User 1293 RepoInfo repoinfo.RepoInfo 1294 Pipelines []models.Pipeline 1295 Active string 1296} 1297 1298func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1299 params.Active = "pipelines" 1300 return p.executeRepo("repo/pipelines/pipelines", w, params) 1301} 1302 1303type LogBlockParams struct { 1304 Id int 1305 Name string 1306 Command string 1307 Collapsed bool 1308} 1309 1310func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1311 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1312} 1313 1314type LogLineParams struct { 1315 Id int 1316 Content string 1317} 1318 1319func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1320 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1321} 1322 1323type WorkflowParams struct { 1324 LoggedInUser *oauth.User 1325 RepoInfo repoinfo.RepoInfo 1326 Pipeline models.Pipeline 1327 Workflow string 1328 LogUrl string 1329 Active string 1330} 1331 1332func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1333 params.Active = "pipelines" 1334 return p.executeRepo("repo/pipelines/workflow", w, params) 1335} 1336 1337type PutStringParams struct { 1338 LoggedInUser *oauth.User 1339 Action string 1340 1341 // this is supplied in the case of editing an existing string 1342 String models.String 1343} 1344 1345func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1346 return p.execute("strings/put", w, params) 1347} 1348 1349type StringsDashboardParams struct { 1350 LoggedInUser *oauth.User 1351 Card ProfileCard 1352 Strings []models.String 1353} 1354 1355func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1356 return p.execute("strings/dashboard", w, params) 1357} 1358 1359type StringTimelineParams struct { 1360 LoggedInUser *oauth.User 1361 Strings []models.String 1362} 1363 1364func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1365 return p.execute("strings/timeline", w, params) 1366} 1367 1368type SingleStringParams struct { 1369 LoggedInUser *oauth.User 1370 ShowRendered bool 1371 RenderToggle bool 1372 RenderedContents template.HTML 1373 String models.String 1374 Stats models.StringStats 1375 Owner identity.Identity 1376} 1377 1378func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1379 var style *chroma.Style = styles.Get("catpuccin-latte") 1380 1381 if params.ShowRendered { 1382 switch markup.GetFormat(params.String.Filename) { 1383 case markup.FormatMarkdown: 1384 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1385 htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1386 sanitized := p.rctx.SanitizeDefault(htmlString) 1387 params.RenderedContents = template.HTML(sanitized) 1388 } 1389 } 1390 1391 c := params.String.Contents 1392 formatter := chromahtml.New( 1393 chromahtml.InlineCode(false), 1394 chromahtml.WithLineNumbers(true), 1395 chromahtml.WithLinkableLineNumbers(true, "L"), 1396 chromahtml.Standalone(false), 1397 chromahtml.WithClasses(true), 1398 ) 1399 1400 lexer := lexers.Get(filepath.Base(params.String.Filename)) 1401 if lexer == nil { 1402 lexer = lexers.Fallback 1403 } 1404 1405 iterator, err := lexer.Tokenise(nil, c) 1406 if err != nil { 1407 return fmt.Errorf("chroma tokenize: %w", err) 1408 } 1409 1410 var code bytes.Buffer 1411 err = formatter.Format(&code, style, iterator) 1412 if err != nil { 1413 return fmt.Errorf("chroma format: %w", err) 1414 } 1415 1416 params.String.Contents = code.String() 1417 return p.execute("strings/string", w, params) 1418} 1419 1420func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1421 return p.execute("timeline/home", w, params) 1422} 1423 1424func (p *Pages) Static() http.Handler { 1425 if p.dev { 1426 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1427 } 1428 1429 sub, err := fs.Sub(Files, "static") 1430 if err != nil { 1431 p.logger.Error("no static dir found? that's crazy", "err", err) 1432 panic(err) 1433 } 1434 // Custom handler to apply Cache-Control headers for font files 1435 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1436} 1437 1438func Cache(h http.Handler) http.Handler { 1439 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1440 path := strings.Split(r.URL.Path, "?")[0] 1441 1442 if strings.HasSuffix(path, ".css") { 1443 // on day for css files 1444 w.Header().Set("Cache-Control", "public, max-age=86400") 1445 } else { 1446 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1447 } 1448 h.ServeHTTP(w, r) 1449 }) 1450} 1451 1452func CssContentHash() string { 1453 cssFile, err := Files.Open("static/tw.css") 1454 if err != nil { 1455 slog.Debug("Error opening CSS file", "err", err) 1456 return "" 1457 } 1458 defer cssFile.Close() 1459 1460 hasher := sha256.New() 1461 if _, err := io.Copy(hasher, cssFile); err != nil { 1462 slog.Debug("Error hashing CSS file", "err", err) 1463 return "" 1464 } 1465 1466 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1467} 1468 1469func (p *Pages) Error500(w io.Writer) error { 1470 return p.execute("errors/500", w, nil) 1471} 1472 1473func (p *Pages) Error404(w io.Writer) error { 1474 return p.execute("errors/404", w, nil) 1475} 1476 1477func (p *Pages) ErrorKnot404(w io.Writer) error { 1478 return p.execute("errors/knot404", w, nil) 1479} 1480 1481func (p *Pages) Error503(w io.Writer) error { 1482 return p.execute("errors/503", w, nil) 1483}