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