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