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