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