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