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