This repository has no description
1package pages
2
3import (
4 "context"
5 "crypto/sha256"
6 "embed"
7 "encoding/hex"
8 "fmt"
9 "html/template"
10 "io"
11 "io/fs"
12 "log/slog"
13 "net/http"
14 "os"
15 "path/filepath"
16 "strings"
17 "sync"
18 "time"
19
20 "tangled.org/core/api/tangled"
21 "tangled.org/core/appview/cache"
22 "tangled.org/core/appview/commitverify"
23 "tangled.org/core/appview/config"
24 "tangled.org/core/appview/db"
25 "tangled.org/core/appview/models"
26 "tangled.org/core/appview/oauth"
27 "tangled.org/core/appview/pages/markup"
28 "tangled.org/core/appview/pages/markup/sanitizer"
29 "tangled.org/core/appview/pages/repoinfo"
30 "tangled.org/core/appview/pagination"
31 "tangled.org/core/idresolver"
32 "tangled.org/core/types"
33
34 "github.com/bluesky-social/indigo/atproto/identity"
35 "github.com/bluesky-social/indigo/atproto/syntax"
36 "github.com/go-git/go-git/v5/plumbing"
37 "github.com/sourcegraph/zoekt"
38)
39
40//go:embed templates/* static legal
41var Files embed.FS
42
43type baseParamsCtxKey struct{}
44
45type BaseParams struct {
46 LoggedInUser *oauth.MultiAccountUser
47 FocusParams FocusParams
48}
49
50type FocusParams struct {
51 Focusing bool
52 FocusLink string
53 FocusNotificationID int64
54 CurrentPath string // r.URL.Path, for off-focus detection in templates
55 FocusCount int // total unread focus-eligible items remaining
56}
57
58func (p *Pages) Resolver() *idresolver.Resolver {
59 return p.resolver
60}
61
62func BaseParamsIntoContext(ctx context.Context, bp BaseParams) context.Context {
63 return context.WithValue(ctx, baseParamsCtxKey{}, bp)
64}
65
66func BaseParamsFromContext(ctx context.Context) BaseParams {
67 bp, _ := ctx.Value(baseParamsCtxKey{}).(BaseParams)
68 return bp
69}
70
71type Pages struct {
72 mu sync.RWMutex
73 cache *TmplCache[string, *template.Template]
74
75 avatar config.AvatarConfig
76 pdsCfg config.PdsConfig
77 resolver *idresolver.Resolver
78 db *db.DB
79 rdb *cache.Cache
80 dev bool
81 embedFS fs.FS
82 templateDir string // Path to templates on disk for dev mode
83 rctx *markup.RenderContext
84 logger *slog.Logger
85}
86
87func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages {
88 // initialized with safe defaults, can be overridden per use
89 rctx := &markup.RenderContext{
90 IsDev: config.Core.Dev,
91 Hostname: config.Core.AppviewHost,
92 CamoUrl: config.Camo.Host,
93 CamoSecret: config.Camo.SharedSecret,
94 Files: Files,
95 }
96
97 p := &Pages{
98 mu: sync.RWMutex{},
99 cache: NewTmplCache[string, *template.Template](),
100 dev: config.Core.Dev,
101 avatar: config.Avatar,
102 pdsCfg: config.Pds,
103 rctx: rctx,
104 resolver: res,
105 db: database,
106 rdb: rdb,
107 templateDir: "appview/pages",
108 logger: logger,
109 }
110
111 if p.dev {
112 p.embedFS = os.DirFS(p.templateDir)
113 } else {
114 p.embedFS = Files
115 }
116
117 return p
118}
119
120// reverse of pathToName
121func (p *Pages) nameToPath(s string) string {
122 return "templates/" + s + ".html"
123}
124
125// FuncMap returns the template function map for use by external template consumers.
126func (p *Pages) FuncMap() template.FuncMap {
127 return p.funcMap()
128}
129
130// FragmentPaths returns all fragment template paths from the embedded FS.
131func (p *Pages) FragmentPaths() ([]string, error) {
132 return p.fragmentPaths()
133}
134
135// EmbedFS returns the embedded filesystem containing templates and static assets.
136func (p *Pages) EmbedFS() fs.FS {
137 return p.embedFS
138}
139
140func (p *Pages) fragmentPaths() ([]string, error) {
141 var fragmentPaths []string
142 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
143 if err != nil {
144 return err
145 }
146 if d.IsDir() {
147 return nil
148 }
149 if !strings.HasSuffix(path, ".html") {
150 return nil
151 }
152 if !strings.Contains(path, "fragments/") {
153 return nil
154 }
155 fragmentPaths = append(fragmentPaths, path)
156 return nil
157 })
158 if err != nil {
159 return nil, err
160 }
161
162 return fragmentPaths, nil
163}
164
165// parse without memoization
166func (p *Pages) rawParse(stack ...string) (*template.Template, error) {
167 paths, err := p.fragmentPaths()
168 if err != nil {
169 return nil, err
170 }
171 for _, s := range stack {
172 paths = append(paths, p.nameToPath(s))
173 }
174
175 funcs := p.funcMap()
176 top := stack[len(stack)-1]
177 parsed, err := template.New(top).
178 Funcs(funcs).
179 ParseFS(p.embedFS, paths...)
180 if err != nil {
181 return nil, err
182 }
183
184 return parsed, nil
185}
186
187func (p *Pages) parse(stack ...string) (*template.Template, error) {
188 key := strings.Join(stack, "|")
189
190 // never cache in dev mode
191 if cached, exists := p.cache.Get(key); !p.dev && exists {
192 return cached, nil
193 }
194
195 result, err := p.rawParse(stack...)
196 if err != nil {
197 return nil, err
198 }
199
200 p.cache.Set(key, result)
201 return result, nil
202}
203
204func (p *Pages) parseBase(top string) (*template.Template, error) {
205 stack := []string{
206 "layouts/base",
207 top,
208 }
209 return p.parse(stack...)
210}
211
212func (p *Pages) parseRepoBase(top string) (*template.Template, error) {
213 stack := []string{
214 "layouts/base",
215 "layouts/repobase",
216 top,
217 }
218 return p.parse(stack...)
219}
220
221func (p *Pages) parseProfileBase(top string) (*template.Template, error) {
222 stack := []string{
223 "layouts/base",
224 "layouts/profilebase",
225 top,
226 }
227 return p.parse(stack...)
228}
229
230func (p *Pages) parseLoginBase(top string) (*template.Template, error) {
231 stack := []string{
232 "layouts/base",
233 "layouts/loginbase",
234 top,
235 }
236 return p.parse(stack...)
237}
238
239func (p *Pages) executePlain(name string, w io.Writer, params any) error {
240 tpl, err := p.parse(name)
241 if err != nil {
242 return err
243 }
244
245 err = tpl.Execute(w, params)
246 if err != nil {
247 p.logger.Error("failed to execute template", "template", name, "err", err)
248 }
249 return err
250}
251
252func (p *Pages) executeLogin(name string, w io.Writer, params any) error {
253 tpl, err := p.parseLoginBase(name)
254 if err != nil {
255 return err
256 }
257
258 err = tpl.ExecuteTemplate(w, "layouts/base", params)
259 if err != nil {
260 p.logger.Error("failed to execute login template", "template", name, "err", err)
261 }
262 return err
263}
264
265func (p *Pages) execute(name string, w io.Writer, params any) error {
266 tpl, err := p.parseBase(name)
267 if err != nil {
268 return err
269 }
270
271 err = tpl.ExecuteTemplate(w, "layouts/base", params)
272 if err != nil {
273 p.logger.Error("failed to execute template", "template", name, "err", err)
274 }
275 return err
276}
277
278func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
279 tpl, err := p.parseRepoBase(name)
280 if err != nil {
281 return err
282 }
283
284 err = tpl.ExecuteTemplate(w, "layouts/base", params)
285 if err != nil {
286 p.logger.Error("failed to execute repo template", "template", name, "err", err)
287 }
288 return err
289}
290
291func (p *Pages) executeProfile(name string, w io.Writer, params any) error {
292 tpl, err := p.parseProfileBase(name)
293 if err != nil {
294 return err
295 }
296
297 err = tpl.ExecuteTemplate(w, "layouts/base", params)
298 if err != nil {
299 p.logger.Error("failed to execute profile template", "template", name, "err", err)
300 }
301 return err
302}
303
304type DollyParams struct {
305 Classes string
306 FillColor string
307 // Favicon embeds a prefers-color-scheme style block so the SVG
308 // adapts to dark mode when used as a standalone favicon document.
309 Favicon bool
310}
311
312func (p *Pages) Dolly(w io.Writer, params DollyParams) error {
313 return p.executePlain("fragments/dolly/logo", w, params)
314}
315
316func (p *Pages) Favicon(w io.Writer) error {
317 return p.Dolly(w, DollyParams{
318 Favicon: true,
319 })
320}
321
322type LoginParams struct {
323 ReturnUrl string
324 ErrorCode string
325 AddAccount bool
326 Accounts []oauth.AccountInfo
327}
328
329func (p *Pages) Login(w io.Writer, params LoginParams) error {
330 return p.executeLogin("user/login", w, params)
331}
332
333type SignupParams struct {
334 CloudflareSiteKey string
335 EmailId string
336}
337
338func (p *Pages) Signup(w io.Writer, params SignupParams) error {
339 return p.executeLogin("user/signup", w, params)
340}
341
342func (p *Pages) CompleteSignup(w io.Writer) error {
343 return p.executeLogin("user/completeSignup", w, nil)
344}
345
346type TermsOfServiceParams struct {
347 BaseParams
348 Content template.HTML
349}
350
351func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error {
352 filename := "terms.md"
353 filePath := filepath.Join("legal", filename)
354
355 file, err := p.embedFS.Open(filePath)
356 if err != nil {
357 return fmt.Errorf("failed to read %s: %w", filename, err)
358 }
359 defer file.Close()
360
361 markdownBytes, err := io.ReadAll(file)
362 if err != nil {
363 return fmt.Errorf("failed to read %s: %w", filename, err)
364 }
365
366 rctx := p.rctx.Clone()
367 rctx.RendererType = markup.RendererTypeDefault
368 htmlString := rctx.RenderMarkdown(string(markdownBytes))
369 sanitized := sanitizer.SanitizeDefault(htmlString)
370 params.Content = template.HTML(sanitized)
371
372 return p.execute("legal/terms", w, params)
373}
374
375type PrivacyPolicyParams struct {
376 BaseParams
377 Content template.HTML
378}
379
380func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error {
381 filename := "privacy.md"
382 filePath := filepath.Join("legal", filename)
383
384 file, err := p.embedFS.Open(filePath)
385 if err != nil {
386 return fmt.Errorf("failed to read %s: %w", filename, err)
387 }
388 defer file.Close()
389
390 markdownBytes, err := io.ReadAll(file)
391 if err != nil {
392 return fmt.Errorf("failed to read %s: %w", filename, err)
393 }
394
395 rctx := p.rctx.Clone()
396 rctx.RendererType = markup.RendererTypeDefault
397 htmlString := rctx.RenderMarkdown(string(markdownBytes))
398 sanitized := sanitizer.SanitizeDefault(htmlString)
399 params.Content = template.HTML(sanitized)
400
401 return p.execute("legal/privacy", w, params)
402}
403
404type BrandParams struct {
405 BaseParams
406}
407
408func (p *Pages) Brand(w io.Writer, params BrandParams) error {
409 return p.execute("brand/brand", w, params)
410}
411
412type RecentItem struct {
413 Link *models.RecentLink
414 Repo *models.Repo
415 Issue *models.Issue
416 Pull *models.Pull
417}
418
419type BlogPost struct {
420 Slug string
421 Title string
422 Subtitle string
423 Date time.Time
424}
425
426type TimelineParams struct {
427 BaseParams
428 Timeline []models.TimelineGroup
429 Repos []models.Repo
430 GfiLabel *models.LabelDefinition
431 BlueskyPosts []models.BskyPost
432 VouchSuggestions []models.VouchSuggestion
433 Notifications []*models.NotificationWithEntity
434 Recents []RecentItem
435 FollowingOnly bool
436 RecentBlogPosts []BlogPost
437 // ShowNewsletter controls whether the newsletter widget/CTA is rendered.
438 // For logged-in users it reflects their newsletter_preferences row; for
439 // anonymous visitors it is always true (dismissal falls back to
440 // localStorage on the client).
441 ShowNewsletter bool
442 CanFocus bool
443}
444
445func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
446 return p.execute("timeline/timeline", w, params)
447}
448
449type GoodFirstIssuesParams struct {
450 BaseParams
451 Issues []models.Issue
452 RepoGroups []*models.RepoGroup
453 LabelDefs map[string]*models.LabelDefinition
454 GfiLabel *models.LabelDefinition
455 Page pagination.Page
456}
457
458func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error {
459 return p.execute("goodfirstissues/index", w, params)
460}
461
462type UserProfileSettingsParams struct {
463 BaseParams
464 Tab string
465 PunchcardPreference models.PunchcardPreference
466 IsTnglSh bool
467 IsDeactivated bool
468 HandleOpen bool
469}
470
471func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error {
472 params.Tab = "profile"
473 return p.execute("user/settings/profile", w, params)
474}
475
476type GroupedNotifications struct {
477 Today []*models.NotificationWithEntity
478 ThisWeek []*models.NotificationWithEntity
479 Older []*models.NotificationWithEntity
480}
481
482func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications {
483 now := time.Now()
484 todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
485 weekStart := todayStart.AddDate(0, 0, -6)
486
487 var g GroupedNotifications
488 for _, n := range notifs {
489 switch {
490 case !n.Created.Before(todayStart):
491 g.Today = append(g.Today, n)
492 case !n.Created.Before(weekStart):
493 g.ThisWeek = append(g.ThisWeek, n)
494 default:
495 g.Older = append(g.Older, n)
496 }
497 }
498 return g
499}
500
501type NotificationsParams struct {
502 BaseParams
503 WorkGroups GroupedNotifications
504 SocialGroups GroupedNotifications
505 MobileGroups GroupedNotifications
506 WorkUnreadCount int64
507 SocialUnreadCount int64
508 Page pagination.Page
509 Total int
510 ReadFilter string // "inbox" or "unread"
511 CategoryFilter string // "all", "work", "social"
512 CanFocus bool
513}
514
515func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error {
516 return p.execute("notifications/list", w, params)
517}
518
519func (p *Pages) NotificationItem(w io.Writer, notif *models.NotificationWithEntity) error {
520 return p.executePlain("notifications/fragments/item", w, notif)
521}
522
523type NotificationCountParams struct {
524 Count int64
525}
526
527func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
528 return p.executePlain("notifications/fragments/count", w, params)
529}
530
531type NotificationPreviewParams struct {
532 BaseParams
533 Notifications []*models.NotificationWithEntity
534 ReadFilter string
535 CategoryFilter string
536 CanFocus bool
537}
538
539func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error {
540 return p.executePlain("notifications/fragments/preview", w, params)
541}
542
543type UserKeysSettingsParams struct {
544 BaseParams
545 PubKeys []models.PublicKey
546 Tab string
547}
548
549func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error {
550 params.Tab = "keys"
551 return p.execute("user/settings/keys", w, params)
552}
553
554type UserEmailsSettingsParams struct {
555 BaseParams
556 Emails []models.Email
557 Tab string
558}
559
560func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error {
561 params.Tab = "emails"
562 return p.execute("user/settings/emails", w, params)
563}
564
565type UserNotificationSettingsParams struct {
566 BaseParams
567 Preferences *models.NotificationPreferences
568 Tab string
569}
570
571func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error {
572 params.Tab = "notifications"
573 return p.execute("user/settings/notifications", w, params)
574}
575
576type UserSiteSettingsParams struct {
577 BaseParams
578 Claim *models.DomainClaim
579 SitesDomain string
580 IsTnglHandle bool
581 Tab string
582}
583
584func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error {
585 params.Tab = "sites"
586 return p.execute("user/settings/sites", w, params)
587}
588
589type UpgradeBannerParams struct {
590 Registrations []models.Registration
591 Spindles []models.Spindle
592}
593
594func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error {
595 return p.executePlain("banner", w, params)
596}
597
598type NewsletterResponseParams struct {
599 // Id identifies the calling form instance; the response span's id will
600 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target.
601 Id string
602 // Error, when non-empty, switches the template to the error variant.
603 Error string
604}
605
606func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error {
607 return p.executePlain("timeline/fragments/newsletterResponse", w, params)
608}
609
610type KnotsParams struct {
611 BaseParams
612 Knots []KnotListingParams
613 Tab string
614}
615
616func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
617 params.Tab = "knots"
618 return p.execute("knots/index", w, params)
619}
620
621type KnotParams struct {
622 BaseParams
623 Registration *models.Registration
624 Members []string
625 Repos map[string][]models.Repo
626 IsOwner bool
627 RepoCount int
628 Tab string
629}
630
631func (p *Pages) Knot(w io.Writer, params KnotParams) error {
632 return p.execute("knots/dashboard", w, params)
633}
634
635type KnotListingParams struct {
636 *models.Registration
637 RepoCount int
638}
639
640func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
641 return p.executePlain("knots/fragments/knotListing", w, params)
642}
643
644type SpindlesParams struct {
645 BaseParams
646 Spindles []models.Spindle
647 Tab string
648}
649
650func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
651 params.Tab = "spindles"
652 return p.execute("spindles/index", w, params)
653}
654
655type SpindleListingParams struct {
656 models.Spindle
657 Tab string
658}
659
660func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
661 return p.executePlain("spindles/fragments/spindleListing", w, params)
662}
663
664type SpindleDashboardParams struct {
665 BaseParams
666 Spindle models.Spindle
667 Members []string
668 Repos map[string][]models.Repo
669 Tab string
670}
671
672func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
673 return p.execute("spindles/dashboard", w, params)
674}
675
676type NewRepoParams struct {
677 BaseParams
678 Knots []string
679}
680
681func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
682 return p.execute("repo/new", w, params)
683}
684
685type ForkRepoParams struct {
686 BaseParams
687 Knots []string
688 RepoInfo repoinfo.RepoInfo
689}
690
691func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
692 return p.execute("repo/fork", w, params)
693}
694
695type ProfileCard struct {
696 UserDid string
697 HasProfile bool
698 FollowStatus models.FollowStatus
699 VouchRelationship *models.VouchRelationship
700 Punchcard *models.Punchcard
701 Profile *models.Profile
702 Stats ProfileStats
703 Active string
704 ProfileScript string
705}
706
707type ProfileStats struct {
708 RepoCount int64
709 StarredCount int64
710 StringCount int64
711 FollowersCount int64
712 FollowingCount int64
713}
714
715func (p *ProfileCard) GetTabs() [][]any {
716 tabs := [][]any{
717 {"overview", "overview", "square-chart-gantt", nil},
718 {"repos", "repos", "book-marked", p.Stats.RepoCount},
719 {"starred", "starred", "star", p.Stats.StarredCount},
720 {"strings", "strings", "line-squiggle", p.Stats.StringCount},
721 {"vouches", "vouches", "shield", nil},
722 }
723
724 return tabs
725}
726
727type ProfileOverviewParams struct {
728 BaseParams
729 Repos []models.Repo
730 CollaboratingRepos []models.Repo
731 ProfileTimeline *models.ProfileTimeline
732 Card *ProfileCard
733 Active string
734 ShowPunchcard bool
735}
736
737func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
738 params.Active = "overview"
739 return p.executeProfile("user/overview", w, params)
740}
741
742type ProfileReposParams struct {
743 BaseParams
744 Repos []models.Repo
745 StarStatuses map[string]bool
746 Card *ProfileCard
747 Active string
748 Page pagination.Page
749 RepoCount int
750 FilterQuery string
751}
752
753func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error {
754 params.Active = "repos"
755 return p.executeProfile("user/repos", w, params)
756}
757
758type ProfileStarredParams struct {
759 BaseParams
760 Repos []models.Repo
761 Card *ProfileCard
762 Page pagination.Page
763 Total int
764 Active string
765}
766
767func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error {
768 params.Active = "starred"
769 return p.executeProfile("user/starred", w, params)
770}
771
772type ProfileStringsParams struct {
773 BaseParams
774 Strings []models.String
775 Card *ProfileCard
776 Active string
777}
778
779func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error {
780 params.Active = "strings"
781 return p.executeProfile("user/strings", w, params)
782}
783
784type ProfileVouchesParams struct {
785 BaseParams
786 Vouches []models.Vouch
787 Suggestions []models.VouchSuggestion
788 Card *ProfileCard
789 Page pagination.Page
790 VouchCount int
791 Active string
792 EvidencePulls map[syntax.ATURI]*models.Pull
793 EvidenceIssues map[syntax.ATURI]*models.Issue
794}
795
796func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error {
797 params.Active = "vouches"
798 return p.executeProfile("user/vouches", w, params)
799}
800
801type FollowCard struct {
802 UserDid string
803 BaseParams
804 FollowStatus models.FollowStatus
805 FollowersCount int64
806 FollowingCount int64
807 Profile *models.Profile
808}
809
810type ProfileFollowersParams struct {
811 BaseParams
812 Followers []FollowCard
813 Card *ProfileCard
814 Active string
815}
816
817func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error {
818 params.Active = "overview"
819 return p.executeProfile("user/followers", w, params)
820}
821
822type ProfileFollowingParams struct {
823 BaseParams
824 Following []FollowCard
825 Card *ProfileCard
826 Active string
827}
828
829func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error {
830 params.Active = "overview"
831 return p.executeProfile("user/following", w, params)
832}
833
834type FollowFragmentParams struct {
835 UserDid string
836 FollowStatus models.FollowStatus
837 FollowersCount int64
838}
839
840func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
841 return p.executePlain("user/fragments/follow-oob", w, params)
842}
843
844type ProfilePopoverParams struct {
845 BaseParams
846 UserDid string
847 Profile *models.Profile
848 FollowStatus models.FollowStatus
849 VouchRelationship *models.VouchRelationship
850 Stats ProfilePopoverStats
851}
852
853type ProfilePopoverStats struct {
854 FollowersCount int64
855 FollowingCount int64
856}
857
858func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error {
859 return p.executePlain("user/fragments/profilePopover", w, params)
860}
861
862type EditBioParams struct {
863 BaseParams
864 Profile *models.Profile
865 AlsoKnownAs []string
866}
867
868func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
869 return p.executePlain("user/fragments/editBio", w, params)
870}
871
872type EditPinsParams struct {
873 BaseParams
874 Profile *models.Profile
875 AllRepos []PinnedRepo
876}
877
878type PinnedRepo struct {
879 IsPinned bool
880 models.Repo
881}
882
883func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
884 return p.executePlain("user/fragments/editPins", w, params)
885}
886
887type StarBtnFragmentParams struct {
888 IsStarred bool
889 SubjectAt syntax.ATURI
890 StarCount int
891 RepoName string
892 HxSwapOob bool
893}
894
895func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
896 params.HxSwapOob = true
897 return p.executePlain("fragments/starBtn", w, params)
898}
899
900type RepoIndexParams struct {
901 BaseParams
902 RepoInfo repoinfo.RepoInfo
903 Active string
904 TagMap map[string][]string
905 CommitsTrunc []types.Commit
906 TagsTrunc []*types.TagReference
907 BranchesTrunc []types.Branch
908 // ForkInfo *types.ForkInfo
909 HTMLReadme template.HTML
910 Raw bool
911 EmailToDid map[string]string
912 VerifiedCommits commitverify.VerifiedCommits
913 Languages []types.RepoLanguageDetails
914 Pipelines map[string]types.Pipeline
915 NeedsKnotUpgrade bool
916 KnotUnreachable bool
917 types.RepoIndexResponse
918}
919
920func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
921 params.Active = "overview"
922 if params.IsEmpty {
923 return p.executeRepo("repo/empty", w, params)
924 }
925
926 if params.NeedsKnotUpgrade {
927 return p.executeRepo("repo/needsUpgrade", w, params)
928 }
929
930 if params.KnotUnreachable {
931 return p.executeRepo("repo/knotUnreachable", w, params)
932 }
933
934 rctx := p.rctx.Clone()
935 rctx.RepoInfo = params.RepoInfo
936 rctx.RepoInfo.Ref = params.Ref
937 rctx.RendererType = markup.RendererTypeRepoMarkdown
938
939 if params.ReadmeFileName != "" {
940 switch markup.GetFormat(params.ReadmeFileName) {
941 case markup.FormatMarkdown:
942 params.Raw = false
943 htmlString := rctx.RenderMarkdown(params.Readme)
944 sanitized := sanitizer.SanitizeDefault(htmlString)
945 params.HTMLReadme = template.HTML(sanitized)
946 default:
947 params.Raw = true
948 }
949 }
950
951 return p.executeRepo("repo/index", w, params)
952}
953
954type RepoSearchParams struct {
955 BaseParams
956 RepoInfo repoinfo.RepoInfo
957 Active string
958 FilterQuery string
959}
960
961func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error {
962 params.Active = "overview"
963 return p.executeRepo("repo/search", w, params)
964}
965
966type RepoSearchResultsFragmentParams struct {
967 Query string
968 Results []SearchResult
969 ErrorMsg string
970}
971
972func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error {
973 return p.executePlain("repo/fragments/searchResults", w, params)
974}
975
976type RepoLogParams struct {
977 BaseParams
978 RepoInfo repoinfo.RepoInfo
979 TagMap map[string][]string
980 Active string
981 EmailToDid map[string]string
982 VerifiedCommits commitverify.VerifiedCommits
983 Pipelines map[string]types.Pipeline
984
985 types.RepoLogResponse
986}
987
988func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
989 params.Active = "overview"
990 return p.executeRepo("repo/log", w, params)
991}
992
993type RepoCommitParams struct {
994 BaseParams
995 RepoInfo repoinfo.RepoInfo
996 Active string
997 EmailToDid map[string]string
998 Pipeline *types.Pipeline
999 DiffOpts types.DiffOpts
1000
1001 // singular because it's always going to be just one
1002 VerifiedCommit commitverify.VerifiedCommits
1003
1004 types.RepoCommitResponse
1005}
1006
1007func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
1008 params.Active = "overview"
1009 return p.executeRepo("repo/commit", w, params)
1010}
1011
1012type RepoTreeParams struct {
1013 BaseParams
1014 RepoInfo repoinfo.RepoInfo
1015 Active string
1016 BreadCrumbs [][]string
1017 Path string
1018 Raw bool
1019 HTMLReadme template.HTML
1020 EmailToDid map[string]string
1021 LastCommitInfo *types.LastCommitInfo
1022 Ref string
1023 Parent string
1024 DotDot string
1025 Files []types.NiceTree
1026 ReadmeFileName string
1027 Readme string
1028}
1029
1030type RepoTreeStats struct {
1031 NumFolders uint64
1032 NumFiles uint64
1033}
1034
1035func (r RepoTreeParams) TreeStats() RepoTreeStats {
1036 numFolders, numFiles := 0, 0
1037 for _, f := range r.Files {
1038 if !f.IsFile() {
1039 numFolders += 1
1040 } else if f.IsFile() {
1041 numFiles += 1
1042 }
1043 }
1044
1045 return RepoTreeStats{
1046 NumFolders: uint64(numFolders),
1047 NumFiles: uint64(numFiles),
1048 }
1049}
1050
1051func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
1052 params.Active = "overview"
1053
1054 rctx := p.rctx.Clone()
1055 rctx.RepoInfo = params.RepoInfo
1056 rctx.RepoInfo.Ref = params.Ref
1057 rctx.RendererType = markup.RendererTypeRepoMarkdown
1058
1059 if params.ReadmeFileName != "" {
1060 switch markup.GetFormat(params.ReadmeFileName) {
1061 case markup.FormatMarkdown:
1062 params.Raw = false
1063 htmlString := rctx.RenderMarkdown(params.Readme)
1064 sanitized := sanitizer.SanitizeDefault(htmlString)
1065 params.HTMLReadme = template.HTML(sanitized)
1066 default:
1067 params.Raw = true
1068 }
1069 }
1070
1071 return p.executeRepo("repo/tree", w, params)
1072}
1073
1074type RepoBranchesParams struct {
1075 BaseParams
1076 RepoInfo repoinfo.RepoInfo
1077 Active string
1078 types.RepoBranchesResponse
1079}
1080
1081func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
1082 params.Active = "overview"
1083 return p.executeRepo("repo/branches", w, params)
1084}
1085
1086type RepoTagsParams struct {
1087 BaseParams
1088 RepoInfo repoinfo.RepoInfo
1089 Active string
1090 types.RepoTagsResponse
1091 ArtifactMap map[plumbing.Hash][]models.Artifact
1092 DanglingArtifacts []models.Artifact
1093}
1094
1095func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
1096 params.Active = "overview"
1097 return p.executeRepo("repo/tags", w, params)
1098}
1099
1100type RepoTagParams struct {
1101 BaseParams
1102 RepoInfo repoinfo.RepoInfo
1103 Active string
1104 types.RepoTagResponse
1105 ArtifactMap map[plumbing.Hash][]models.Artifact
1106 DanglingArtifacts []models.Artifact
1107}
1108
1109func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error {
1110 params.Active = "overview"
1111 return p.executeRepo("repo/tag", w, params)
1112}
1113
1114type RepoArtifactParams struct {
1115 BaseParams
1116 RepoInfo repoinfo.RepoInfo
1117 Artifact models.Artifact
1118}
1119
1120func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
1121 return p.executePlain("repo/fragments/artifact", w, params)
1122}
1123
1124type RepoBlobParams struct {
1125 BaseParams
1126 RepoInfo repoinfo.RepoInfo
1127 Active string // always "overview"
1128 BreadCrumbs [][]string
1129 BlobView models.BlobView // TODO: expose this struct
1130 ShowRendered bool
1131 EmailToDid map[string]string
1132 LastCommitInfo *types.LastCommitInfo
1133 Ref string
1134 Path string
1135 Language string
1136}
1137
1138func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
1139 params.Active = "overview"
1140 return p.executeRepo("repo/blob", w, params)
1141}
1142
1143type Collaborator struct {
1144 Did string
1145 Role string
1146}
1147
1148type RepoSettingsParams struct {
1149 BaseParams
1150 RepoInfo repoinfo.RepoInfo
1151 Collaborators []Collaborator
1152 Active string
1153 Branches []types.Branch
1154 Spindles []string
1155 CurrentSpindle string
1156 Secrets []*tangled.RepoListSecrets_Secret
1157
1158 // TODO: use repoinfo.roles
1159 IsCollaboratorInviteAllowed bool
1160}
1161
1162func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
1163 params.Active = "settings"
1164 return p.executeRepo("repo/settings", w, params)
1165}
1166
1167type RepoGeneralSettingsParams struct {
1168 BaseParams
1169 RepoInfo repoinfo.RepoInfo
1170 Labels []models.LabelDefinition
1171 DefaultLabels []models.LabelDefinition
1172 SubscribedLabels map[string]struct{}
1173 ShouldSubscribeAll bool
1174 Active string
1175 Tab string
1176 Branches []types.Branch
1177}
1178
1179func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
1180 params.Active = "settings"
1181 params.Tab = "general"
1182 return p.executeRepo("repo/settings/general", w, params)
1183}
1184
1185type RepoAccessSettingsParams struct {
1186 BaseParams
1187 RepoInfo repoinfo.RepoInfo
1188 Active string
1189 Tab string
1190 Collaborators []Collaborator
1191 CanRemoveCollaborator bool
1192}
1193
1194func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
1195 params.Active = "settings"
1196 params.Tab = "access"
1197 return p.executeRepo("repo/settings/access", w, params)
1198}
1199
1200type RepoPipelineSettingsParams struct {
1201 BaseParams
1202 RepoInfo repoinfo.RepoInfo
1203 Active string
1204 Tab string
1205 Spindles []string
1206 CurrentSpindle string
1207 Secrets []map[string]any
1208}
1209
1210func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
1211 params.Active = "settings"
1212 params.Tab = "pipelines"
1213 return p.executeRepo("repo/settings/pipelines", w, params)
1214}
1215
1216type RepoWebhooksSettingsParams struct {
1217 BaseParams
1218 RepoInfo repoinfo.RepoInfo
1219 Active string
1220 Tab string
1221 Webhooks []models.Webhook
1222 WebhookDeliveries map[int64][]models.WebhookDelivery
1223}
1224
1225func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error {
1226 params.Active = "settings"
1227 params.Tab = "hooks"
1228 return p.executeRepo("repo/settings/hooks", w, params)
1229}
1230
1231type WebhookDeliveriesListParams struct {
1232 BaseParams
1233 RepoInfo repoinfo.RepoInfo
1234 Webhook *models.Webhook
1235 Deliveries []models.WebhookDelivery
1236}
1237
1238func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error {
1239 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries")
1240 if err != nil {
1241 return err
1242 }
1243 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params)
1244}
1245
1246type RepoSiteSettingsParams struct {
1247 BaseParams
1248 RepoInfo repoinfo.RepoInfo
1249 Active string
1250 Tab string
1251 Branches []types.Branch
1252 SiteConfig *models.RepoSite
1253 OwnerClaim *models.DomainClaim
1254 Deploys []models.SiteDeploy
1255 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or ""
1256}
1257
1258func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error {
1259 params.Active = "settings"
1260 params.Tab = "sites"
1261 return p.executeRepo("repo/settings/sites", w, params)
1262}
1263
1264type RepoIssuesParams struct {
1265 BaseParams
1266 RepoInfo repoinfo.RepoInfo
1267 Active string
1268 Issues []models.Issue
1269 IssueCount int
1270 LabelDefs map[string]*models.LabelDefinition
1271 Page pagination.Page
1272 FilterState string
1273 FilterQuery string
1274 BaseFilterQuery string
1275 VouchRelationships map[syntax.DID]*models.VouchRelationship
1276}
1277
1278func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
1279 params.Active = "issues"
1280 return p.executeRepo("repo/issues/issues", w, params)
1281}
1282
1283type RepoSingleIssueParams struct {
1284 BaseParams
1285 RepoInfo repoinfo.RepoInfo
1286 Active string
1287 Issue *models.Issue
1288 CommentList []models.CommentListItem
1289 Backlinks []models.RichReferenceLink
1290 LabelDefs map[string]*models.LabelDefinition
1291
1292 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1293 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1294 VouchRelationships map[syntax.DID]*models.VouchRelationship
1295}
1296
1297func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
1298 params.Active = "issues"
1299 return p.executeRepo("repo/issues/issue", w, params)
1300}
1301
1302type EditIssueParams struct {
1303 BaseParams
1304 RepoInfo repoinfo.RepoInfo
1305 Issue *models.Issue
1306 Action string
1307}
1308
1309func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error {
1310 params.Action = "edit"
1311 return p.executePlain("repo/issues/fragments/putIssue", w, params)
1312}
1313
1314type ThreadReactionFragmentParams struct {
1315 Kind models.ReactionKind
1316 Count int
1317 Users []string
1318 IsReacted bool
1319 CommentRkey string
1320 SubjectUri string
1321}
1322
1323func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error {
1324 return p.executePlain("repo/fragments/reaction", w, params)
1325}
1326
1327type RepoNewIssueParams struct {
1328 BaseParams
1329 RepoInfo repoinfo.RepoInfo
1330 Issue *models.Issue // existing issue if any -- passed when editing
1331 Active string
1332 Action string
1333}
1334
1335func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
1336 params.Active = "issues"
1337 params.Action = "create"
1338 return p.executeRepo("repo/issues/new", w, params)
1339}
1340
1341type StackedDiff struct {
1342 Diff *types.NiceDiff
1343 Opts types.DiffOpts
1344}
1345
1346type RepoNewPullParams struct {
1347 BaseParams
1348 RepoInfo repoinfo.RepoInfo
1349 Branches []types.Branch
1350 SourceBranches []types.Branch
1351 ForkBranches []types.Branch
1352 Forks []models.Repo
1353 Source Source
1354 SourceBranch string
1355 TargetBranch string
1356 Fork string
1357 Patch string
1358 Title string
1359 Body string
1360 TitleDirty bool
1361 BodyDirty bool
1362 IsStacked bool
1363 Comparison *types.RepoFormatPatchResponse
1364 Diff *types.NiceDiff
1365 DiffOpts types.DiffOpts
1366 StackedDiffs []StackedDiff
1367 MergeCheck *types.MergeCheckResponse
1368 StackTitles map[string]string
1369 StackBodies map[string]string
1370 PrefillError string
1371 Active string
1372 LabelDefs map[string]*models.LabelDefinition
1373 LabelState models.LabelState
1374 StackLabelStates map[string]models.LabelState
1375}
1376
1377func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
1378 params.Active = "pulls"
1379 return p.executeRepo("repo/pulls/new", w, params)
1380}
1381
1382func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error {
1383 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params)
1384}
1385
1386func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error {
1387 return p.executePlain("fragments/markdownPreview", w, body)
1388}
1389
1390type RepoPullsParams struct {
1391 BaseParams
1392 RepoInfo repoinfo.RepoInfo
1393 Pulls []*models.Pull
1394 Active string
1395 FilterState string
1396 FilterQuery string
1397 BaseFilterQuery string
1398 Stacks []models.Stack
1399 Pipelines map[string]types.Pipeline
1400 LabelDefs map[string]*models.LabelDefinition
1401 Page pagination.Page
1402 PullCount int
1403 VouchRelationships map[syntax.DID]*models.VouchRelationship
1404}
1405
1406func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
1407 params.Active = "pulls"
1408 return p.executeRepo("repo/pulls/pulls", w, params)
1409}
1410
1411type ResubmitResult uint64
1412
1413const (
1414 ShouldResubmit ResubmitResult = iota
1415 ShouldNotResubmit
1416 Unknown
1417)
1418
1419func (r ResubmitResult) Yes() bool {
1420 return r == ShouldResubmit
1421}
1422func (r ResubmitResult) No() bool {
1423 return r == ShouldNotResubmit
1424}
1425func (r ResubmitResult) Unknown() bool {
1426 return r == Unknown
1427}
1428
1429type RepoSinglePullParams struct {
1430 BaseParams
1431 RepoInfo repoinfo.RepoInfo
1432 Active string
1433 Pull *models.Pull
1434 Stack models.Stack
1435 Backlinks []models.RichReferenceLink
1436 BranchDeleteStatus *models.BranchDeleteStatus
1437 MergeCheck types.MergeCheckResponse
1438 ResubmitCheck ResubmitResult
1439 Pipelines map[string]types.Pipeline
1440 Diff types.DiffRenderer
1441 DiffOpts types.DiffOpts
1442 ActiveRound int
1443 IsInterdiff bool
1444
1445 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1446 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1447
1448 LabelDefs map[string]*models.LabelDefinition
1449 VouchRelationships map[syntax.DID]*models.VouchRelationship
1450 VouchSkips map[syntax.DID]bool
1451}
1452
1453func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1454 params.Active = "pulls"
1455 return p.executeRepo("repo/pulls/pull", w, params)
1456}
1457
1458type PullResubmitParams struct {
1459 BaseParams
1460 RepoInfo repoinfo.RepoInfo
1461 Pull *models.Pull
1462 SubmissionId int
1463}
1464
1465func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1466 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1467}
1468
1469type PullActionsParams struct {
1470 BaseParams
1471 RepoInfo repoinfo.RepoInfo
1472 Pull *models.Pull
1473 RoundNumber int
1474 MergeCheck types.MergeCheckResponse
1475 ResubmitCheck ResubmitResult
1476 BranchDeleteStatus *models.BranchDeleteStatus
1477 Stack models.Stack
1478
1479 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1480 // that fetches the real, checked fragment
1481 Loading bool
1482}
1483
1484func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1485 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1486}
1487
1488type PullNewCommentParams struct {
1489 BaseParams
1490 RepoInfo repoinfo.RepoInfo
1491 Pull *models.Pull
1492 RoundNumber int
1493}
1494
1495func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
1496 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
1497}
1498
1499type RepoCompareParams struct {
1500 BaseParams
1501 RepoInfo repoinfo.RepoInfo
1502 Forks []models.Repo
1503 Branches []types.Branch
1504 Tags []*types.TagReference
1505 Base string
1506 Head string
1507 Diff *types.NiceDiff
1508 DiffOpts types.DiffOpts
1509
1510 Active string
1511}
1512
1513func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1514 params.Active = "overview"
1515 return p.executeRepo("repo/compare/compare", w, params)
1516}
1517
1518type RepoCompareNewParams struct {
1519 BaseParams
1520 RepoInfo repoinfo.RepoInfo
1521 Forks []models.Repo
1522 Branches []types.Branch
1523 Tags []*types.TagReference
1524 Base string
1525 Head string
1526
1527 Active string
1528}
1529
1530func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1531 params.Active = "overview"
1532 return p.executeRepo("repo/compare/new", w, params)
1533}
1534
1535type RepoCompareAllowPullParams struct {
1536 BaseParams
1537 RepoInfo repoinfo.RepoInfo
1538 Base string
1539 Head string
1540}
1541
1542func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1543 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1544}
1545
1546type RepoCompareDiffFragmentParams struct {
1547 Diff types.NiceDiff
1548 DiffOpts types.DiffOpts
1549}
1550
1551func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1552 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1553}
1554
1555type LabelPanelParams struct {
1556 BaseParams
1557 RepoInfo repoinfo.RepoInfo
1558 Defs map[string]*models.LabelDefinition
1559 Subject string
1560 State models.LabelState
1561}
1562
1563func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1564 return p.executePlain("repo/fragments/labelPanel", w, params)
1565}
1566
1567type EditLabelPanelParams struct {
1568 BaseParams
1569 RepoInfo repoinfo.RepoInfo
1570 Defs map[string]*models.LabelDefinition
1571 Subject string
1572 State models.LabelState
1573 Prefix string
1574}
1575
1576func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1577 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1578}
1579
1580type RepoStarsParams struct {
1581 BaseParams
1582 RepoInfo repoinfo.RepoInfo
1583 Active string
1584 Starrers []models.Star
1585 Page pagination.Page
1586 TotalCount int
1587}
1588
1589func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1590 params.Active = "overview"
1591 return p.executeRepo("repo/stars", w, params)
1592}
1593
1594type RepoForksParams struct {
1595 BaseParams
1596 RepoInfo repoinfo.RepoInfo
1597 Active string
1598 Forks []models.Repo
1599 Page pagination.Page
1600 TotalCount int
1601}
1602
1603func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1604 params.Active = "overview"
1605 return p.executeRepo("repo/forks", w, params)
1606}
1607
1608type PipelinesParams struct {
1609 BaseParams
1610 RepoInfo repoinfo.RepoInfo
1611 Pipelines []types.Pipeline
1612 Active string
1613 FilterKind string
1614 Total int64
1615}
1616
1617func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1618 params.Active = "pipelines"
1619 return p.executeRepo("repo/pipelines/pipelines", w, params)
1620}
1621
1622type LogBlockParams struct {
1623 Id int
1624 Name string
1625 Command string
1626 Collapsed bool
1627 StartTime time.Time
1628}
1629
1630func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1631 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1632}
1633
1634type LogBlockEndParams struct {
1635 Id int
1636 StartTime time.Time
1637 EndTime time.Time
1638}
1639
1640func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1641 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1642}
1643
1644type LogLineParams struct {
1645 Id int
1646 Content template.HTML
1647}
1648
1649func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1650 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1651}
1652
1653type WorkflowSymbolOOBParams struct {
1654 Name string
1655 Statuses models.WorkflowStatus
1656}
1657
1658func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1659 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1660}
1661
1662type WorkflowParams struct {
1663 BaseParams
1664 RepoInfo repoinfo.RepoInfo
1665 Pipeline types.Pipeline
1666 Workflow string
1667 LogUrl string
1668 Active string
1669}
1670
1671func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1672 params.Active = "pipelines"
1673 return p.executeRepo("repo/pipelines/workflow", w, params)
1674}
1675
1676type PutStringParams struct {
1677 BaseParams
1678 Action string
1679
1680 // this is supplied in the case of editing an existing string
1681 String models.String
1682}
1683
1684func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1685 return p.execute("strings/put", w, params)
1686}
1687
1688type StringsDashboardParams struct {
1689 BaseParams
1690 Card ProfileCard
1691 Strings []models.String
1692}
1693
1694func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1695 return p.execute("strings/dashboard", w, params)
1696}
1697
1698type StringTimelineParams struct {
1699 BaseParams
1700 Strings []models.String
1701}
1702
1703func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1704 return p.execute("strings/timeline", w, params)
1705}
1706
1707type SingleStringParams struct {
1708 BaseParams
1709 ShowRendered bool
1710 RenderToggle bool
1711 RenderedContents template.HTML
1712 String *models.String
1713 Stats models.StringStats
1714 IsStarred bool
1715 StarCount int
1716 Owner identity.Identity
1717 CommentList []models.CommentListItem
1718
1719 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1720 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1721 VouchRelationships map[syntax.DID]*models.VouchRelationship
1722}
1723
1724func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1725 return p.execute("strings/string", w, params)
1726}
1727
1728type SearchReposParams struct {
1729 BaseParams
1730 FilterType string // "repo" | "code"
1731 Repos []SearchResult
1732 Page pagination.Page
1733 ResultCount int
1734 FilterQuery string
1735 SortParam string
1736 TimeTaken time.Duration
1737 DocCount int64
1738 ErrorMsg string
1739}
1740
1741func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1742 params.FilterType = "repo"
1743 return p.execute("search/search", w, params)
1744}
1745
1746type SearchQuickParams struct {
1747 Repos []models.Repo
1748 Query string
1749 Total int
1750}
1751
1752func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1753 return p.executePlain("search/fragments/quick", w, params)
1754}
1755
1756func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1757 tpl, err := p.parse("search/fragments/quick")
1758 if err != nil {
1759 return err
1760 }
1761 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1762}
1763
1764type SearchResult struct {
1765 RepoDID syntax.DID
1766 Repo *models.Repo
1767 FilePath string
1768 Branches []string
1769 Commit string
1770 Language string
1771
1772 File *CodeSearchResult_File // filename match
1773 Chunks CodeSearchResult_Chunks // content matches
1774}
1775
1776// CodeSearchResult_Chunk is a content match with its lines pre-rendered.
1777type CodeSearchResult_Chunk struct {
1778 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges
1779 MatchCount int // number of match ranges in this chunk
1780}
1781
1782type CodeSearchResult_Chunks []CodeSearchResult_Chunk
1783
1784func (cs CodeSearchResult_Chunks) MatchCount() int {
1785 count := 0
1786 for _, c := range cs {
1787 count += c.MatchCount
1788 }
1789 return count
1790}
1791
1792type CodeSearchResult_File struct {
1793 NameSpans []ChunkSpan // precomputed from FilePath/Ranges
1794}
1795
1796type ChunkSpan struct {
1797 Text string
1798 Match bool
1799}
1800
1801type ChunkLine struct {
1802 Num int
1803 Spans []ChunkSpan
1804 Highlight bool
1805}
1806
1807// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each
1808// line into matched/unmatched spans using ranges. startLine is the 1-based line
1809// number of the first line.
1810func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine {
1811 if startLine < 1 {
1812 startLine = 1
1813 }
1814 // trim a single trailing newline so we don't emit a spurious empty line
1815 content = strings.TrimSuffix(content, "\n")
1816 lines := strings.Split(content, "\n")
1817 out := make([]ChunkLine, len(lines))
1818 for i, text := range lines {
1819 num := startLine + i
1820 runes := []rune(text)
1821
1822 // collect matched rune intervals [c0,c1) for this line
1823 var intervals [][2]int
1824 for _, rg := range ranges {
1825 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) {
1826 continue
1827 }
1828 c0, c1 := 0, len(runes)
1829 if num == int(rg.Start.LineNumber) {
1830 c0 = int(rg.Start.Column) - 1
1831 }
1832 if num == int(rg.End.LineNumber) {
1833 c1 = int(rg.End.Column) - 1
1834 }
1835 c0 = max(0, min(c0, len(runes)))
1836 c1 = max(0, min(c1, len(runes)))
1837 if c0 < c1 {
1838 intervals = append(intervals, [2]int{c0, c1})
1839 }
1840 }
1841 intervals = mergeIntervals(intervals)
1842
1843 out[i] = ChunkLine{
1844 Num: num,
1845 Spans: spanRunes(runes, intervals),
1846 Highlight: len(intervals) > 0,
1847 }
1848 }
1849 return out
1850}
1851
1852// FileNameSpans splits a filename into matched/unmatched spans using ranges.
1853// Filename ranges live on line 1; columns are clamped to rune bounds.
1854func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan {
1855 runes := []rune(name)
1856 var intervals [][2]int
1857 for _, rg := range ranges {
1858 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 {
1859 continue
1860 }
1861 c0 := max(0, min(int(rg.Start.Column)-1, len(runes)))
1862 c1 := max(0, min(int(rg.End.Column)-1, len(runes)))
1863 if c0 < c1 {
1864 intervals = append(intervals, [2]int{c0, c1})
1865 }
1866 }
1867 return spanRunes(runes, mergeIntervals(intervals))
1868}
1869
1870type CodeSearchParams struct {
1871 BaseParams
1872 FilterType string // "code"
1873 FilterQuery string
1874 Results []SearchResult
1875 Page pagination.Page
1876 HasMore bool
1877 ErrorMsg string
1878
1879 MatchCount int
1880 FileCount int
1881 TimeTaken time.Duration
1882}
1883
1884func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error {
1885 params.FilterType = "code"
1886 return p.execute("search/search", w, params)
1887}
1888
1889func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1890 return p.execute("timeline/home", w, params)
1891}
1892
1893type CommentBodyFragmentParams struct {
1894 Comment models.Comment
1895 Reactions map[models.ReactionKind]models.ReactionDisplayData
1896 UserReacted map[models.ReactionKind]bool
1897}
1898
1899func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1900 return p.executePlain("fragments/comment/commentBody", w, params)
1901}
1902
1903type PullCommentFragmentParams struct {
1904 LoggedInUser *oauth.MultiAccountUser
1905 Comment models.Comment
1906 Reactions map[models.ReactionKind]models.ReactionDisplayData
1907 UserReacted map[models.ReactionKind]bool
1908 HxSwapOob bool
1909}
1910
1911func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error {
1912 return p.executePlain("fragments/comment/pullComment", w, params)
1913}
1914
1915type CommentHeaderFragmentParams struct {
1916 Comment models.Comment
1917 Reactions map[models.ReactionKind]models.ReactionDisplayData
1918 UserReacted map[models.ReactionKind]bool
1919 HxSwapOob bool
1920}
1921
1922func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1923 return p.executePlain("fragments/comment/commentHeader", w, params)
1924}
1925
1926type EditCommentFragmentParams struct {
1927 Comment models.Comment
1928}
1929
1930func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1931 return p.executePlain("fragments/comment/edit", w, params)
1932}
1933
1934type ReplyCommentFragmentParams struct {
1935 BaseParams
1936}
1937
1938func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1939 return p.executePlain("fragments/comment/reply", w, params)
1940}
1941
1942type ReplyPlaceholderFragmentParams struct {
1943 BaseParams
1944}
1945
1946func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
1947 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
1948}
1949
1950func (p *Pages) Static() http.Handler {
1951 if p.dev {
1952 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1953 }
1954
1955 sub, err := fs.Sub(p.embedFS, "static")
1956 if err != nil {
1957 p.logger.Error("no static dir found? that's crazy", "err", err)
1958 panic(err)
1959 }
1960 // Custom handler to apply Cache-Control headers for font files
1961 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1962}
1963
1964func Cache(h http.Handler) http.Handler {
1965 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1966 path := strings.Split(r.URL.Path, "?")[0]
1967
1968 if strings.HasSuffix(path, ".css") {
1969 // on day for css files
1970 w.Header().Set("Cache-Control", "public, max-age=86400")
1971 } else {
1972 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1973 }
1974 h.ServeHTTP(w, r)
1975 })
1976}
1977
1978func (p *Pages) CssContentHash() string {
1979 cssFile, err := p.embedFS.Open("static/tw.css")
1980 if err != nil {
1981 slog.Debug("Error opening CSS file", "err", err)
1982 return ""
1983 }
1984 defer cssFile.Close()
1985
1986 hasher := sha256.New()
1987 if _, err := io.Copy(hasher, cssFile); err != nil {
1988 slog.Debug("Error hashing CSS file", "err", err)
1989 return ""
1990 }
1991
1992 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1993}
1994
1995func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
1996 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
1997}
1998
1999func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
2000 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
2001}
2002
2003func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
2004 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
2005}
2006
2007func (p *Pages) Error500(w io.Writer) error {
2008 return p.execute("errors/500", w, nil)
2009}
2010
2011func (p *Pages) Error404(w io.Writer) error {
2012 return p.execute("errors/404", w, nil)
2013}
2014
2015func (p *Pages) ErrorKnot404(w io.Writer) error {
2016 return p.execute("errors/knot404", w, nil)
2017}
2018
2019func (p *Pages) Error503(w io.Writer) error {
2020 return p.execute("errors/503", w, nil)
2021}