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