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