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