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