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