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