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