Monorepo for Tangled
tangled.org
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 // WorkflowsChanged and ChangedWorkflowFiles describe whether the latest
1446 // round's patch touches .tangled/workflows/, for warning maintainers
1447 // before they manually trigger CI on a fork-based pull request.
1448 WorkflowsChanged bool
1449 ChangedWorkflowFiles []string
1450
1451 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1452 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1453
1454 LabelDefs map[string]*models.LabelDefinition
1455 VouchRelationships map[syntax.DID]*models.VouchRelationship
1456 VouchSkips map[syntax.DID]bool
1457}
1458
1459func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1460 params.Active = "pulls"
1461 return p.executeRepo("repo/pulls/pull", w, params)
1462}
1463
1464type PullResubmitParams struct {
1465 BaseParams
1466 RepoInfo repoinfo.RepoInfo
1467 Pull *models.Pull
1468 SubmissionId int
1469}
1470
1471func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1472 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1473}
1474
1475type PullActionsParams struct {
1476 BaseParams
1477 RepoInfo repoinfo.RepoInfo
1478 Pull *models.Pull
1479 RoundNumber int
1480 MergeCheck types.MergeCheckResponse
1481 ResubmitCheck ResubmitResult
1482 BranchDeleteStatus *models.BranchDeleteStatus
1483 Stack models.Stack
1484
1485 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1486 // that fetches the real, checked fragment
1487 Loading bool
1488}
1489
1490func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1491 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1492}
1493
1494type PullNewCommentParams struct {
1495 BaseParams
1496 RepoInfo repoinfo.RepoInfo
1497 Pull *models.Pull
1498 RoundNumber int
1499}
1500
1501func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
1502 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
1503}
1504
1505type RepoCompareParams struct {
1506 BaseParams
1507 RepoInfo repoinfo.RepoInfo
1508 Forks []models.Repo
1509 Branches []types.Branch
1510 Tags []*types.TagReference
1511 Base string
1512 Head string
1513 Diff *types.NiceDiff
1514 DiffOpts types.DiffOpts
1515
1516 Active string
1517}
1518
1519func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1520 params.Active = "overview"
1521 return p.executeRepo("repo/compare/compare", w, params)
1522}
1523
1524type RepoCompareNewParams struct {
1525 BaseParams
1526 RepoInfo repoinfo.RepoInfo
1527 Forks []models.Repo
1528 Branches []types.Branch
1529 Tags []*types.TagReference
1530 Base string
1531 Head string
1532
1533 Active string
1534}
1535
1536func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1537 params.Active = "overview"
1538 return p.executeRepo("repo/compare/new", w, params)
1539}
1540
1541type RepoCompareAllowPullParams struct {
1542 BaseParams
1543 RepoInfo repoinfo.RepoInfo
1544 Base string
1545 Head string
1546}
1547
1548func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1549 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1550}
1551
1552type RepoCompareDiffFragmentParams struct {
1553 Diff types.NiceDiff
1554 DiffOpts types.DiffOpts
1555}
1556
1557func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1558 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1559}
1560
1561type LabelPanelParams struct {
1562 BaseParams
1563 RepoInfo repoinfo.RepoInfo
1564 Defs map[string]*models.LabelDefinition
1565 Subject string
1566 State models.LabelState
1567}
1568
1569func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1570 return p.executePlain("repo/fragments/labelPanel", w, params)
1571}
1572
1573type EditLabelPanelParams struct {
1574 BaseParams
1575 RepoInfo repoinfo.RepoInfo
1576 Defs map[string]*models.LabelDefinition
1577 Subject string
1578 State models.LabelState
1579 Prefix string
1580}
1581
1582func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1583 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1584}
1585
1586type RepoStarsParams struct {
1587 BaseParams
1588 RepoInfo repoinfo.RepoInfo
1589 Active string
1590 Starrers []models.Star
1591 Page pagination.Page
1592 TotalCount int
1593}
1594
1595func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1596 params.Active = "overview"
1597 return p.executeRepo("repo/stars", w, params)
1598}
1599
1600type RepoForksParams struct {
1601 BaseParams
1602 RepoInfo repoinfo.RepoInfo
1603 Active string
1604 Forks []models.Repo
1605 Page pagination.Page
1606 TotalCount int
1607}
1608
1609func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1610 params.Active = "overview"
1611 return p.executeRepo("repo/forks", w, params)
1612}
1613
1614type PipelinesParams struct {
1615 BaseParams
1616 RepoInfo repoinfo.RepoInfo
1617 Pipelines []types.Pipeline
1618 Active string
1619 FilterKind string
1620 Total int64
1621}
1622
1623func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1624 params.Active = "pipelines"
1625 return p.executeRepo("repo/pipelines/pipelines", w, params)
1626}
1627
1628type LogBlockParams struct {
1629 Id int
1630 Name string
1631 Command string
1632 Collapsed bool
1633 StartTime time.Time
1634}
1635
1636func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1637 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1638}
1639
1640type LogBlockEndParams struct {
1641 Id int
1642 StartTime time.Time
1643 EndTime time.Time
1644}
1645
1646func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1647 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1648}
1649
1650type LogLineParams struct {
1651 Id int
1652 Content template.HTML
1653}
1654
1655func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1656 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1657}
1658
1659type WorkflowSymbolOOBParams struct {
1660 Name string
1661 Statuses models.WorkflowStatus
1662}
1663
1664func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1665 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1666}
1667
1668type WorkflowParams struct {
1669 BaseParams
1670 RepoInfo repoinfo.RepoInfo
1671 Pipeline types.Pipeline
1672 Workflow string
1673 LogUrl string
1674 Active string
1675}
1676
1677func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1678 params.Active = "pipelines"
1679 return p.executeRepo("repo/pipelines/workflow", w, params)
1680}
1681
1682type PutStringParams struct {
1683 BaseParams
1684 Action string
1685
1686 // this is supplied in the case of editing an existing string
1687 String models.String
1688}
1689
1690func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1691 return p.execute("strings/put", w, params)
1692}
1693
1694type StringsDashboardParams struct {
1695 BaseParams
1696 Card ProfileCard
1697 Strings []models.String
1698}
1699
1700func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1701 return p.execute("strings/dashboard", w, params)
1702}
1703
1704type StringTimelineParams struct {
1705 BaseParams
1706 Strings []models.String
1707}
1708
1709func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1710 return p.execute("strings/timeline", w, params)
1711}
1712
1713type SingleStringParams struct {
1714 BaseParams
1715 ShowRendered bool
1716 RenderToggle bool
1717 RenderedContents template.HTML
1718 String *models.String
1719 Stats models.StringStats
1720 IsStarred bool
1721 StarCount int
1722 Owner identity.Identity
1723 CommentList []models.CommentListItem
1724
1725 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1726 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1727 VouchRelationships map[syntax.DID]*models.VouchRelationship
1728}
1729
1730func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1731 return p.execute("strings/string", w, params)
1732}
1733
1734type SearchReposParams struct {
1735 BaseParams
1736 FilterType string // "repo" | "code"
1737 Repos []SearchResult
1738 Page pagination.Page
1739 ResultCount int
1740 FilterQuery string
1741 SortParam string
1742 TimeTaken time.Duration
1743 DocCount int64
1744 ErrorMsg string
1745}
1746
1747func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1748 params.FilterType = "repo"
1749 return p.execute("search/search", w, params)
1750}
1751
1752type SearchQuickParams struct {
1753 Repos []models.Repo
1754 Query string
1755 Total int
1756}
1757
1758func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1759 return p.executePlain("search/fragments/quick", w, params)
1760}
1761
1762func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1763 tpl, err := p.parse("search/fragments/quick")
1764 if err != nil {
1765 return err
1766 }
1767 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1768}
1769
1770type SearchResult struct {
1771 RepoDID syntax.DID
1772 Repo *models.Repo
1773 FilePath string
1774 Branches []string
1775 Commit string
1776 Language string
1777
1778 File *CodeSearchResult_File // filename match
1779 Chunks CodeSearchResult_Chunks // content matches
1780}
1781
1782// CodeSearchResult_Chunk is a content match with its lines pre-rendered.
1783type CodeSearchResult_Chunk struct {
1784 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges
1785 MatchCount int // number of match ranges in this chunk
1786}
1787
1788type CodeSearchResult_Chunks []CodeSearchResult_Chunk
1789
1790func (cs CodeSearchResult_Chunks) MatchCount() int {
1791 count := 0
1792 for _, c := range cs {
1793 count += c.MatchCount
1794 }
1795 return count
1796}
1797
1798type CodeSearchResult_File struct {
1799 NameSpans []ChunkSpan // precomputed from FilePath/Ranges
1800}
1801
1802type ChunkSpan struct {
1803 Text string
1804 Match bool
1805}
1806
1807type ChunkLine struct {
1808 Num int
1809 Spans []ChunkSpan
1810 Highlight bool
1811}
1812
1813// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each
1814// line into matched/unmatched spans using ranges. startLine is the 1-based line
1815// number of the first line.
1816func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine {
1817 if startLine < 1 {
1818 startLine = 1
1819 }
1820 // trim a single trailing newline so we don't emit a spurious empty line
1821 content = strings.TrimSuffix(content, "\n")
1822 lines := strings.Split(content, "\n")
1823 out := make([]ChunkLine, len(lines))
1824 for i, text := range lines {
1825 num := startLine + i
1826 runes := []rune(text)
1827
1828 // collect matched rune intervals [c0,c1) for this line
1829 var intervals [][2]int
1830 for _, rg := range ranges {
1831 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) {
1832 continue
1833 }
1834 c0, c1 := 0, len(runes)
1835 if num == int(rg.Start.LineNumber) {
1836 c0 = int(rg.Start.Column) - 1
1837 }
1838 if num == int(rg.End.LineNumber) {
1839 c1 = int(rg.End.Column) - 1
1840 }
1841 c0 = max(0, min(c0, len(runes)))
1842 c1 = max(0, min(c1, len(runes)))
1843 if c0 < c1 {
1844 intervals = append(intervals, [2]int{c0, c1})
1845 }
1846 }
1847 intervals = mergeIntervals(intervals)
1848
1849 out[i] = ChunkLine{
1850 Num: num,
1851 Spans: spanRunes(runes, intervals),
1852 Highlight: len(intervals) > 0,
1853 }
1854 }
1855 return out
1856}
1857
1858// FileNameSpans splits a filename into matched/unmatched spans using ranges.
1859// Filename ranges live on line 1; columns are clamped to rune bounds.
1860func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan {
1861 runes := []rune(name)
1862 var intervals [][2]int
1863 for _, rg := range ranges {
1864 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 {
1865 continue
1866 }
1867 c0 := max(0, min(int(rg.Start.Column)-1, len(runes)))
1868 c1 := max(0, min(int(rg.End.Column)-1, len(runes)))
1869 if c0 < c1 {
1870 intervals = append(intervals, [2]int{c0, c1})
1871 }
1872 }
1873 return spanRunes(runes, mergeIntervals(intervals))
1874}
1875
1876type CodeSearchParams struct {
1877 BaseParams
1878 FilterType string // "code"
1879 FilterQuery string
1880 Results []SearchResult
1881 Page pagination.Page
1882 HasMore bool
1883 ErrorMsg string
1884
1885 MatchCount int
1886 FileCount int
1887 TimeTaken time.Duration
1888}
1889
1890func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error {
1891 params.FilterType = "code"
1892 return p.execute("search/search", w, params)
1893}
1894
1895func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1896 return p.execute("timeline/home", w, params)
1897}
1898
1899type CommentBodyFragmentParams struct {
1900 Comment models.Comment
1901 Reactions map[models.ReactionKind]models.ReactionDisplayData
1902 UserReacted map[models.ReactionKind]bool
1903}
1904
1905func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1906 return p.executePlain("fragments/comment/commentBody", w, params)
1907}
1908
1909type PullCommentFragmentParams struct {
1910 LoggedInUser *oauth.MultiAccountUser
1911 Comment models.Comment
1912 Reactions map[models.ReactionKind]models.ReactionDisplayData
1913 UserReacted map[models.ReactionKind]bool
1914 HxSwapOob bool
1915}
1916
1917func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error {
1918 return p.executePlain("fragments/comment/pullComment", w, params)
1919}
1920
1921type CommentHeaderFragmentParams struct {
1922 Comment models.Comment
1923 Reactions map[models.ReactionKind]models.ReactionDisplayData
1924 UserReacted map[models.ReactionKind]bool
1925 HxSwapOob bool
1926}
1927
1928func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1929 return p.executePlain("fragments/comment/commentHeader", w, params)
1930}
1931
1932type EditCommentFragmentParams struct {
1933 Comment models.Comment
1934}
1935
1936func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1937 return p.executePlain("fragments/comment/edit", w, params)
1938}
1939
1940type ReplyCommentFragmentParams struct {
1941 BaseParams
1942}
1943
1944func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1945 return p.executePlain("fragments/comment/reply", w, params)
1946}
1947
1948type ReplyPlaceholderFragmentParams struct {
1949 BaseParams
1950}
1951
1952func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
1953 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
1954}
1955
1956func (p *Pages) Static() http.Handler {
1957 if p.dev {
1958 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1959 }
1960
1961 sub, err := fs.Sub(p.embedFS, "static")
1962 if err != nil {
1963 p.logger.Error("no static dir found? that's crazy", "err", err)
1964 panic(err)
1965 }
1966 // Custom handler to apply Cache-Control headers for font files
1967 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1968}
1969
1970func Cache(h http.Handler) http.Handler {
1971 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1972 path := strings.Split(r.URL.Path, "?")[0]
1973
1974 if strings.HasSuffix(path, ".css") {
1975 // on day for css files
1976 w.Header().Set("Cache-Control", "public, max-age=86400")
1977 } else {
1978 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1979 }
1980 h.ServeHTTP(w, r)
1981 })
1982}
1983
1984func (p *Pages) CssContentHash() string {
1985 cssFile, err := p.embedFS.Open("static/tw.css")
1986 if err != nil {
1987 slog.Debug("Error opening CSS file", "err", err)
1988 return ""
1989 }
1990 defer cssFile.Close()
1991
1992 hasher := sha256.New()
1993 if _, err := io.Copy(hasher, cssFile); err != nil {
1994 slog.Debug("Error hashing CSS file", "err", err)
1995 return ""
1996 }
1997
1998 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1999}
2000
2001func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
2002 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
2003}
2004
2005func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
2006 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
2007}
2008
2009func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
2010 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
2011}
2012
2013func (p *Pages) Error500(w io.Writer) error {
2014 return p.execute("errors/500", w, nil)
2015}
2016
2017func (p *Pages) Error404(w io.Writer) error {
2018 return p.execute("errors/404", w, nil)
2019}
2020
2021func (p *Pages) ErrorKnot404(w io.Writer) error {
2022 return p.execute("errors/knot404", w, nil)
2023}
2024
2025func (p *Pages) Error503(w io.Writer) error {
2026 return p.execute("errors/503", w, nil)
2027}