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}
685
686type ProfileStats struct {
687 RepoCount int64
688 StarredCount int64
689 StringCount int64
690 FollowersCount int64
691 FollowingCount int64
692}
693
694func (p *ProfileCard) GetTabs() [][]any {
695 tabs := [][]any{
696 {"overview", "overview", "square-chart-gantt", nil},
697 {"repos", "repos", "book-marked", p.Stats.RepoCount},
698 {"starred", "starred", "star", p.Stats.StarredCount},
699 {"strings", "strings", "line-squiggle", p.Stats.StringCount},
700 {"vouches", "vouches", "shield", nil},
701 }
702
703 return tabs
704}
705
706type ProfileOverviewParams struct {
707 BaseParams
708 Repos []models.Repo
709 CollaboratingRepos []models.Repo
710 ProfileTimeline *models.ProfileTimeline
711 Card *ProfileCard
712 Active string
713 ShowPunchcard bool
714}
715
716func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
717 params.Active = "overview"
718 return p.executeProfile("user/overview", w, params)
719}
720
721type ProfileReposParams struct {
722 BaseParams
723 Repos []models.Repo
724 StarStatuses map[string]bool
725 Card *ProfileCard
726 Active string
727 Page pagination.Page
728 RepoCount int
729 FilterQuery string
730}
731
732func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error {
733 params.Active = "repos"
734 return p.executeProfile("user/repos", w, params)
735}
736
737type ProfileStarredParams struct {
738 BaseParams
739 Repos []models.Repo
740 Card *ProfileCard
741 Page pagination.Page
742 Total int
743 Active string
744}
745
746func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error {
747 params.Active = "starred"
748 return p.executeProfile("user/starred", w, params)
749}
750
751type ProfileStringsParams struct {
752 BaseParams
753 Strings []models.String
754 Card *ProfileCard
755 Active string
756}
757
758func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error {
759 params.Active = "strings"
760 return p.executeProfile("user/strings", w, params)
761}
762
763type ProfileVouchesParams struct {
764 BaseParams
765 Vouches []models.Vouch
766 Suggestions []models.VouchSuggestion
767 Card *ProfileCard
768 Page pagination.Page
769 VouchCount int
770 Active string
771 EvidencePulls map[syntax.ATURI]*models.Pull
772 EvidenceIssues map[syntax.ATURI]*models.Issue
773}
774
775func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error {
776 params.Active = "vouches"
777 return p.executeProfile("user/vouches", w, params)
778}
779
780type FollowCard struct {
781 UserDid string
782 BaseParams
783 FollowStatus models.FollowStatus
784 FollowersCount int64
785 FollowingCount int64
786 Profile *models.Profile
787}
788
789type ProfileFollowersParams struct {
790 BaseParams
791 Followers []FollowCard
792 Card *ProfileCard
793 Active string
794}
795
796func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error {
797 params.Active = "overview"
798 return p.executeProfile("user/followers", w, params)
799}
800
801type ProfileFollowingParams struct {
802 BaseParams
803 Following []FollowCard
804 Card *ProfileCard
805 Active string
806}
807
808func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error {
809 params.Active = "overview"
810 return p.executeProfile("user/following", w, params)
811}
812
813type FollowFragmentParams struct {
814 UserDid string
815 FollowStatus models.FollowStatus
816 FollowersCount int64
817}
818
819func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
820 return p.executePlain("user/fragments/follow-oob", w, params)
821}
822
823type ProfilePopoverParams struct {
824 BaseParams
825 UserDid string
826 Profile *models.Profile
827 FollowStatus models.FollowStatus
828 VouchRelationship *models.VouchRelationship
829 Stats ProfilePopoverStats
830}
831
832type ProfilePopoverStats struct {
833 FollowersCount int64
834 FollowingCount int64
835}
836
837func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error {
838 return p.executePlain("user/fragments/profilePopover", w, params)
839}
840
841type EditBioParams struct {
842 BaseParams
843 Profile *models.Profile
844 AlsoKnownAs []string
845}
846
847func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
848 return p.executePlain("user/fragments/editBio", w, params)
849}
850
851type EditPinsParams struct {
852 BaseParams
853 Profile *models.Profile
854 AllRepos []PinnedRepo
855}
856
857type PinnedRepo struct {
858 IsPinned bool
859 models.Repo
860}
861
862func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
863 return p.executePlain("user/fragments/editPins", w, params)
864}
865
866type StarBtnFragmentParams struct {
867 IsStarred bool
868 SubjectAt syntax.ATURI
869 StarCount int
870 RepoName string
871 HxSwapOob bool
872}
873
874func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
875 params.HxSwapOob = true
876 return p.executePlain("fragments/starBtn", w, params)
877}
878
879type RepoIndexParams struct {
880 BaseParams
881 RepoInfo repoinfo.RepoInfo
882 Active string
883 TagMap map[string][]string
884 CommitsTrunc []types.Commit
885 TagsTrunc []*types.TagReference
886 BranchesTrunc []types.Branch
887 // ForkInfo *types.ForkInfo
888 HTMLReadme template.HTML
889 Raw bool
890 EmailToDid map[string]string
891 VerifiedCommits commitverify.VerifiedCommits
892 Languages []types.RepoLanguageDetails
893 Pipelines map[string]models.Pipeline
894 NeedsKnotUpgrade bool
895 KnotUnreachable bool
896 types.RepoIndexResponse
897}
898
899func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
900 params.Active = "overview"
901 if params.IsEmpty {
902 return p.executeRepo("repo/empty", w, params)
903 }
904
905 if params.NeedsKnotUpgrade {
906 return p.executeRepo("repo/needsUpgrade", w, params)
907 }
908
909 if params.KnotUnreachable {
910 return p.executeRepo("repo/knotUnreachable", w, params)
911 }
912
913 rctx := p.rctx.Clone()
914 rctx.RepoInfo = params.RepoInfo
915 rctx.RepoInfo.Ref = params.Ref
916 rctx.RendererType = markup.RendererTypeRepoMarkdown
917
918 if params.ReadmeFileName != "" {
919 switch markup.GetFormat(params.ReadmeFileName) {
920 case markup.FormatMarkdown:
921 params.Raw = false
922 htmlString := rctx.RenderMarkdown(params.Readme)
923 sanitized := sanitizer.SanitizeDefault(htmlString)
924 params.HTMLReadme = template.HTML(sanitized)
925 default:
926 params.Raw = true
927 }
928 }
929
930 return p.executeRepo("repo/index", w, params)
931}
932
933type RepoSearchParams struct {
934 BaseParams
935 RepoInfo repoinfo.RepoInfo
936 Active string
937 FilterQuery string
938}
939
940func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error {
941 params.Active = "overview"
942 return p.executeRepo("repo/search", w, params)
943}
944
945type RepoSearchResultsFragmentParams struct {
946 Query string
947 Results []SearchResult
948 ErrorMsg string
949}
950
951func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error {
952 return p.executePlain("repo/fragments/searchResults", w, params)
953}
954
955type RepoLogParams struct {
956 BaseParams
957 RepoInfo repoinfo.RepoInfo
958 TagMap map[string][]string
959 Active string
960 EmailToDid map[string]string
961 VerifiedCommits commitverify.VerifiedCommits
962 Pipelines map[string]models.Pipeline
963
964 types.RepoLogResponse
965}
966
967func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
968 params.Active = "overview"
969 return p.executeRepo("repo/log", w, params)
970}
971
972type RepoCommitParams struct {
973 BaseParams
974 RepoInfo repoinfo.RepoInfo
975 Active string
976 EmailToDid map[string]string
977 Pipeline *models.Pipeline
978 DiffOpts types.DiffOpts
979
980 // singular because it's always going to be just one
981 VerifiedCommit commitverify.VerifiedCommits
982
983 types.RepoCommitResponse
984}
985
986func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
987 params.Active = "overview"
988 return p.executeRepo("repo/commit", w, params)
989}
990
991type RepoTreeParams struct {
992 BaseParams
993 RepoInfo repoinfo.RepoInfo
994 Active string
995 BreadCrumbs [][]string
996 Path string
997 Raw bool
998 HTMLReadme template.HTML
999 EmailToDid map[string]string
1000 LastCommitInfo *types.LastCommitInfo
1001 Ref string
1002 Parent string
1003 DotDot string
1004 Files []types.NiceTree
1005 ReadmeFileName string
1006 Readme string
1007}
1008
1009type RepoTreeStats struct {
1010 NumFolders uint64
1011 NumFiles uint64
1012}
1013
1014func (r RepoTreeParams) TreeStats() RepoTreeStats {
1015 numFolders, numFiles := 0, 0
1016 for _, f := range r.Files {
1017 if !f.IsFile() {
1018 numFolders += 1
1019 } else if f.IsFile() {
1020 numFiles += 1
1021 }
1022 }
1023
1024 return RepoTreeStats{
1025 NumFolders: uint64(numFolders),
1026 NumFiles: uint64(numFiles),
1027 }
1028}
1029
1030func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
1031 params.Active = "overview"
1032
1033 rctx := p.rctx.Clone()
1034 rctx.RepoInfo = params.RepoInfo
1035 rctx.RepoInfo.Ref = params.Ref
1036 rctx.RendererType = markup.RendererTypeRepoMarkdown
1037
1038 if params.ReadmeFileName != "" {
1039 switch markup.GetFormat(params.ReadmeFileName) {
1040 case markup.FormatMarkdown:
1041 params.Raw = false
1042 htmlString := rctx.RenderMarkdown(params.Readme)
1043 sanitized := sanitizer.SanitizeDefault(htmlString)
1044 params.HTMLReadme = template.HTML(sanitized)
1045 default:
1046 params.Raw = true
1047 }
1048 }
1049
1050 return p.executeRepo("repo/tree", w, params)
1051}
1052
1053type RepoBranchesParams struct {
1054 BaseParams
1055 RepoInfo repoinfo.RepoInfo
1056 Active string
1057 types.RepoBranchesResponse
1058}
1059
1060func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
1061 params.Active = "overview"
1062 return p.executeRepo("repo/branches", w, params)
1063}
1064
1065type RepoTagsParams struct {
1066 BaseParams
1067 RepoInfo repoinfo.RepoInfo
1068 Active string
1069 types.RepoTagsResponse
1070 ArtifactMap map[plumbing.Hash][]models.Artifact
1071 DanglingArtifacts []models.Artifact
1072}
1073
1074func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
1075 params.Active = "overview"
1076 return p.executeRepo("repo/tags", w, params)
1077}
1078
1079type RepoTagParams struct {
1080 BaseParams
1081 RepoInfo repoinfo.RepoInfo
1082 Active string
1083 types.RepoTagResponse
1084 ArtifactMap map[plumbing.Hash][]models.Artifact
1085 DanglingArtifacts []models.Artifact
1086}
1087
1088func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error {
1089 params.Active = "overview"
1090 return p.executeRepo("repo/tag", w, params)
1091}
1092
1093type RepoArtifactParams struct {
1094 BaseParams
1095 RepoInfo repoinfo.RepoInfo
1096 Artifact models.Artifact
1097}
1098
1099func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
1100 return p.executePlain("repo/fragments/artifact", w, params)
1101}
1102
1103type RepoBlobParams struct {
1104 BaseParams
1105 RepoInfo repoinfo.RepoInfo
1106 Active string // always "overview"
1107 BreadCrumbs [][]string
1108 BlobView models.BlobView // TODO: expose this struct
1109 ShowRendered bool
1110 EmailToDid map[string]string
1111 LastCommitInfo *types.LastCommitInfo
1112 Ref string
1113 Path string
1114}
1115
1116func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
1117 params.Active = "overview"
1118 return p.executeRepo("repo/blob", w, params)
1119}
1120
1121type Collaborator struct {
1122 Did string
1123 Role string
1124}
1125
1126type RepoSettingsParams struct {
1127 BaseParams
1128 RepoInfo repoinfo.RepoInfo
1129 Collaborators []Collaborator
1130 Active string
1131 Branches []types.Branch
1132 Spindles []string
1133 CurrentSpindle string
1134 Secrets []*tangled.RepoListSecrets_Secret
1135
1136 // TODO: use repoinfo.roles
1137 IsCollaboratorInviteAllowed bool
1138}
1139
1140func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
1141 params.Active = "settings"
1142 return p.executeRepo("repo/settings", w, params)
1143}
1144
1145type RepoGeneralSettingsParams struct {
1146 BaseParams
1147 RepoInfo repoinfo.RepoInfo
1148 Labels []models.LabelDefinition
1149 DefaultLabels []models.LabelDefinition
1150 SubscribedLabels map[string]struct{}
1151 ShouldSubscribeAll bool
1152 Active string
1153 Tab string
1154 Branches []types.Branch
1155}
1156
1157func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
1158 params.Active = "settings"
1159 params.Tab = "general"
1160 return p.executeRepo("repo/settings/general", w, params)
1161}
1162
1163type RepoAccessSettingsParams struct {
1164 BaseParams
1165 RepoInfo repoinfo.RepoInfo
1166 Active string
1167 Tab string
1168 Collaborators []Collaborator
1169 CanRemoveCollaborator bool
1170}
1171
1172func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
1173 params.Active = "settings"
1174 params.Tab = "access"
1175 return p.executeRepo("repo/settings/access", w, params)
1176}
1177
1178type RepoPipelineSettingsParams struct {
1179 BaseParams
1180 RepoInfo repoinfo.RepoInfo
1181 Active string
1182 Tab string
1183 Spindles []string
1184 CurrentSpindle string
1185 Secrets []map[string]any
1186}
1187
1188func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
1189 params.Active = "settings"
1190 params.Tab = "pipelines"
1191 return p.executeRepo("repo/settings/pipelines", w, params)
1192}
1193
1194type RepoWebhooksSettingsParams struct {
1195 BaseParams
1196 RepoInfo repoinfo.RepoInfo
1197 Active string
1198 Tab string
1199 Webhooks []models.Webhook
1200 WebhookDeliveries map[int64][]models.WebhookDelivery
1201}
1202
1203func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error {
1204 params.Active = "settings"
1205 params.Tab = "hooks"
1206 return p.executeRepo("repo/settings/hooks", w, params)
1207}
1208
1209type WebhookDeliveriesListParams struct {
1210 BaseParams
1211 RepoInfo repoinfo.RepoInfo
1212 Webhook *models.Webhook
1213 Deliveries []models.WebhookDelivery
1214}
1215
1216func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error {
1217 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries")
1218 if err != nil {
1219 return err
1220 }
1221 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params)
1222}
1223
1224type RepoSiteSettingsParams struct {
1225 BaseParams
1226 RepoInfo repoinfo.RepoInfo
1227 Active string
1228 Tab string
1229 Branches []types.Branch
1230 SiteConfig *models.RepoSite
1231 OwnerClaim *models.DomainClaim
1232 Deploys []models.SiteDeploy
1233 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or ""
1234}
1235
1236func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error {
1237 params.Active = "settings"
1238 params.Tab = "sites"
1239 return p.executeRepo("repo/settings/sites", w, params)
1240}
1241
1242type RepoIssuesParams struct {
1243 BaseParams
1244 RepoInfo repoinfo.RepoInfo
1245 Active string
1246 Issues []models.Issue
1247 IssueCount int
1248 LabelDefs map[string]*models.LabelDefinition
1249 Page pagination.Page
1250 FilterState string
1251 FilterQuery string
1252 BaseFilterQuery string
1253 VouchRelationships map[syntax.DID]*models.VouchRelationship
1254}
1255
1256func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
1257 params.Active = "issues"
1258 return p.executeRepo("repo/issues/issues", w, params)
1259}
1260
1261type RepoSingleIssueParams struct {
1262 BaseParams
1263 RepoInfo repoinfo.RepoInfo
1264 Active string
1265 Issue *models.Issue
1266 CommentList []models.CommentListItem
1267 Backlinks []models.RichReferenceLink
1268 LabelDefs map[string]*models.LabelDefinition
1269
1270 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1271 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1272 VouchRelationships map[syntax.DID]*models.VouchRelationship
1273}
1274
1275func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
1276 params.Active = "issues"
1277 return p.executeRepo("repo/issues/issue", w, params)
1278}
1279
1280type EditIssueParams struct {
1281 BaseParams
1282 RepoInfo repoinfo.RepoInfo
1283 Issue *models.Issue
1284 Action string
1285}
1286
1287func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error {
1288 params.Action = "edit"
1289 return p.executePlain("repo/issues/fragments/putIssue", w, params)
1290}
1291
1292type ThreadReactionFragmentParams struct {
1293 Kind models.ReactionKind
1294 Count int
1295 Users []string
1296 IsReacted bool
1297 CommentRkey string
1298 SubjectUri string
1299}
1300
1301func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error {
1302 return p.executePlain("repo/fragments/reaction", w, params)
1303}
1304
1305type RepoNewIssueParams struct {
1306 BaseParams
1307 RepoInfo repoinfo.RepoInfo
1308 Issue *models.Issue // existing issue if any -- passed when editing
1309 Active string
1310 Action string
1311}
1312
1313func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
1314 params.Active = "issues"
1315 params.Action = "create"
1316 return p.executeRepo("repo/issues/new", w, params)
1317}
1318
1319type StackedDiff struct {
1320 Diff *types.NiceDiff
1321 Opts types.DiffOpts
1322}
1323
1324type RepoNewPullParams struct {
1325 BaseParams
1326 RepoInfo repoinfo.RepoInfo
1327 Branches []types.Branch
1328 SourceBranches []types.Branch
1329 ForkBranches []types.Branch
1330 Forks []models.Repo
1331 Source Source
1332 SourceBranch string
1333 TargetBranch string
1334 Fork string
1335 Patch string
1336 Title string
1337 Body string
1338 TitleDirty bool
1339 BodyDirty bool
1340 IsStacked bool
1341 Comparison *types.RepoFormatPatchResponse
1342 Diff *types.NiceDiff
1343 DiffOpts types.DiffOpts
1344 StackedDiffs []StackedDiff
1345 MergeCheck *types.MergeCheckResponse
1346 StackTitles map[string]string
1347 StackBodies map[string]string
1348 PrefillError string
1349 Active string
1350 LabelDefs map[string]*models.LabelDefinition
1351 LabelState models.LabelState
1352 StackLabelStates map[string]models.LabelState
1353}
1354
1355func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
1356 params.Active = "pulls"
1357 return p.executeRepo("repo/pulls/new", w, params)
1358}
1359
1360func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error {
1361 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params)
1362}
1363
1364func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error {
1365 return p.executePlain("fragments/markdownPreview", w, body)
1366}
1367
1368type RepoPullsParams struct {
1369 BaseParams
1370 RepoInfo repoinfo.RepoInfo
1371 Pulls []*models.Pull
1372 Active string
1373 FilterState string
1374 FilterQuery string
1375 BaseFilterQuery string
1376 Stacks []models.Stack
1377 Pipelines map[string]models.Pipeline
1378 LabelDefs map[string]*models.LabelDefinition
1379 Page pagination.Page
1380 PullCount int
1381 VouchRelationships map[syntax.DID]*models.VouchRelationship
1382}
1383
1384func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
1385 params.Active = "pulls"
1386 return p.executeRepo("repo/pulls/pulls", w, params)
1387}
1388
1389type ResubmitResult uint64
1390
1391const (
1392 ShouldResubmit ResubmitResult = iota
1393 ShouldNotResubmit
1394 Unknown
1395)
1396
1397func (r ResubmitResult) Yes() bool {
1398 return r == ShouldResubmit
1399}
1400func (r ResubmitResult) No() bool {
1401 return r == ShouldNotResubmit
1402}
1403func (r ResubmitResult) Unknown() bool {
1404 return r == Unknown
1405}
1406
1407type RepoSinglePullParams struct {
1408 BaseParams
1409 RepoInfo repoinfo.RepoInfo
1410 Active string
1411 Pull *models.Pull
1412 Stack models.Stack
1413 Backlinks []models.RichReferenceLink
1414 BranchDeleteStatus *models.BranchDeleteStatus
1415 MergeCheck types.MergeCheckResponse
1416 ResubmitCheck ResubmitResult
1417 Pipelines map[string]models.Pipeline
1418 Diff types.DiffRenderer
1419 DiffOpts types.DiffOpts
1420 ActiveRound int
1421 IsInterdiff bool
1422
1423 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1424 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1425
1426 LabelDefs map[string]*models.LabelDefinition
1427 VouchRelationships map[syntax.DID]*models.VouchRelationship
1428 VouchSkips map[syntax.DID]bool
1429}
1430
1431func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1432 params.Active = "pulls"
1433 return p.executeRepo("repo/pulls/pull", w, params)
1434}
1435
1436type PullResubmitParams struct {
1437 BaseParams
1438 RepoInfo repoinfo.RepoInfo
1439 Pull *models.Pull
1440 SubmissionId int
1441}
1442
1443func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1444 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1445}
1446
1447type PullActionsParams struct {
1448 BaseParams
1449 RepoInfo repoinfo.RepoInfo
1450 Pull *models.Pull
1451 RoundNumber int
1452 MergeCheck types.MergeCheckResponse
1453 ResubmitCheck ResubmitResult
1454 BranchDeleteStatus *models.BranchDeleteStatus
1455 Stack models.Stack
1456
1457 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1458 // that fetches the real, checked fragment
1459 Loading bool
1460}
1461
1462func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1463 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1464}
1465
1466type PullNewCommentParams struct {
1467 BaseParams
1468 RepoInfo repoinfo.RepoInfo
1469 Pull *models.Pull
1470 RoundNumber int
1471}
1472
1473func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
1474 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
1475}
1476
1477type RepoCompareParams struct {
1478 BaseParams
1479 RepoInfo repoinfo.RepoInfo
1480 Forks []models.Repo
1481 Branches []types.Branch
1482 Tags []*types.TagReference
1483 Base string
1484 Head string
1485 Diff *types.NiceDiff
1486 DiffOpts types.DiffOpts
1487
1488 Active string
1489}
1490
1491func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1492 params.Active = "overview"
1493 return p.executeRepo("repo/compare/compare", w, params)
1494}
1495
1496type RepoCompareNewParams struct {
1497 BaseParams
1498 RepoInfo repoinfo.RepoInfo
1499 Forks []models.Repo
1500 Branches []types.Branch
1501 Tags []*types.TagReference
1502 Base string
1503 Head string
1504
1505 Active string
1506}
1507
1508func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1509 params.Active = "overview"
1510 return p.executeRepo("repo/compare/new", w, params)
1511}
1512
1513type RepoCompareAllowPullParams struct {
1514 BaseParams
1515 RepoInfo repoinfo.RepoInfo
1516 Base string
1517 Head string
1518}
1519
1520func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1521 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1522}
1523
1524type RepoCompareDiffFragmentParams struct {
1525 Diff types.NiceDiff
1526 DiffOpts types.DiffOpts
1527}
1528
1529func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1530 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1531}
1532
1533type LabelPanelParams struct {
1534 BaseParams
1535 RepoInfo repoinfo.RepoInfo
1536 Defs map[string]*models.LabelDefinition
1537 Subject string
1538 State models.LabelState
1539}
1540
1541func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1542 return p.executePlain("repo/fragments/labelPanel", w, params)
1543}
1544
1545type EditLabelPanelParams struct {
1546 BaseParams
1547 RepoInfo repoinfo.RepoInfo
1548 Defs map[string]*models.LabelDefinition
1549 Subject string
1550 State models.LabelState
1551 Prefix string
1552}
1553
1554func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1555 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1556}
1557
1558type RepoStarsParams struct {
1559 BaseParams
1560 RepoInfo repoinfo.RepoInfo
1561 Active string
1562 Starrers []models.Star
1563 Page pagination.Page
1564 TotalCount int
1565}
1566
1567func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1568 params.Active = "overview"
1569 return p.executeRepo("repo/stars", w, params)
1570}
1571
1572type RepoForksParams struct {
1573 BaseParams
1574 RepoInfo repoinfo.RepoInfo
1575 Active string
1576 Forks []models.Repo
1577 Page pagination.Page
1578 TotalCount int
1579}
1580
1581func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1582 params.Active = "overview"
1583 return p.executeRepo("repo/forks", w, params)
1584}
1585
1586type PipelinesParams struct {
1587 BaseParams
1588 RepoInfo repoinfo.RepoInfo
1589 Pipelines []models.Pipeline
1590 Active string
1591 FilterKind string
1592 Total int64
1593}
1594
1595func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1596 params.Active = "pipelines"
1597 return p.executeRepo("repo/pipelines/pipelines", w, params)
1598}
1599
1600type LogBlockParams struct {
1601 Id int
1602 Name string
1603 Command string
1604 Collapsed bool
1605 StartTime time.Time
1606}
1607
1608func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1609 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1610}
1611
1612type LogBlockEndParams struct {
1613 Id int
1614 StartTime time.Time
1615 EndTime time.Time
1616}
1617
1618func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1619 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1620}
1621
1622type LogLineParams struct {
1623 Id int
1624 Content template.HTML
1625}
1626
1627func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1628 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1629}
1630
1631type WorkflowSymbolOOBParams struct {
1632 Name string
1633 Statuses models.WorkflowStatus
1634}
1635
1636func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1637 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1638}
1639
1640type WorkflowParams struct {
1641 BaseParams
1642 RepoInfo repoinfo.RepoInfo
1643 Pipeline models.Pipeline
1644 Workflow string
1645 LogUrl string
1646 Active string
1647}
1648
1649func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1650 params.Active = "pipelines"
1651 return p.executeRepo("repo/pipelines/workflow", w, params)
1652}
1653
1654type PutStringParams struct {
1655 BaseParams
1656 Action string
1657
1658 // this is supplied in the case of editing an existing string
1659 String models.String
1660}
1661
1662func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1663 return p.execute("strings/put", w, params)
1664}
1665
1666type StringsDashboardParams struct {
1667 BaseParams
1668 Card ProfileCard
1669 Strings []models.String
1670}
1671
1672func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1673 return p.execute("strings/dashboard", w, params)
1674}
1675
1676type StringTimelineParams struct {
1677 BaseParams
1678 Strings []models.String
1679}
1680
1681func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1682 return p.execute("strings/timeline", w, params)
1683}
1684
1685type SingleStringParams struct {
1686 BaseParams
1687 ShowRendered bool
1688 RenderToggle bool
1689 RenderedContents template.HTML
1690 String *models.String
1691 Stats models.StringStats
1692 IsStarred bool
1693 StarCount int
1694 Owner identity.Identity
1695 CommentList []models.CommentListItem
1696
1697 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1698 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1699 VouchRelationships map[syntax.DID]*models.VouchRelationship
1700}
1701
1702func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1703 return p.execute("strings/string", w, params)
1704}
1705
1706type SearchReposParams struct {
1707 BaseParams
1708 FilterType string // "repo" | "code"
1709 Repos []SearchResult
1710 Page pagination.Page
1711 ResultCount int
1712 FilterQuery string
1713 SortParam string
1714 TimeTaken time.Duration
1715 DocCount int64
1716 ErrorMsg string
1717}
1718
1719func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1720 params.FilterType = "repo"
1721 return p.execute("search/search", w, params)
1722}
1723
1724type SearchQuickParams struct {
1725 Repos []models.Repo
1726 Query string
1727 Total int
1728}
1729
1730func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1731 return p.executePlain("search/fragments/quick", w, params)
1732}
1733
1734func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1735 tpl, err := p.parse("search/fragments/quick")
1736 if err != nil {
1737 return err
1738 }
1739 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1740}
1741
1742type SearchResult struct {
1743 RepoDID syntax.DID
1744 Repo *models.Repo
1745 FilePath string
1746 Branches []string
1747 Commit string
1748 Language string
1749
1750 File *CodeSearchResult_File // filename match
1751 Chunks CodeSearchResult_Chunks // content matches
1752}
1753
1754// CodeSearchResult_Chunk is a content match with its lines pre-rendered.
1755type CodeSearchResult_Chunk struct {
1756 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges
1757 MatchCount int // number of match ranges in this chunk
1758}
1759
1760type CodeSearchResult_Chunks []CodeSearchResult_Chunk
1761
1762func (cs CodeSearchResult_Chunks) MatchCount() int {
1763 count := 0
1764 for _, c := range cs {
1765 count += c.MatchCount
1766 }
1767 return count
1768}
1769
1770type CodeSearchResult_File struct {
1771 NameSpans []ChunkSpan // precomputed from FilePath/Ranges
1772}
1773
1774type ChunkSpan struct {
1775 Text string
1776 Match bool
1777}
1778
1779type ChunkLine struct {
1780 Num int
1781 Spans []ChunkSpan
1782 Highlight bool
1783}
1784
1785// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each
1786// line into matched/unmatched spans using ranges. startLine is the 1-based line
1787// number of the first line.
1788func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine {
1789 if startLine < 1 {
1790 startLine = 1
1791 }
1792 // trim a single trailing newline so we don't emit a spurious empty line
1793 content = strings.TrimSuffix(content, "\n")
1794 lines := strings.Split(content, "\n")
1795 out := make([]ChunkLine, len(lines))
1796 for i, text := range lines {
1797 num := startLine + i
1798 runes := []rune(text)
1799
1800 // collect matched rune intervals [c0,c1) for this line
1801 var intervals [][2]int
1802 for _, rg := range ranges {
1803 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) {
1804 continue
1805 }
1806 c0, c1 := 0, len(runes)
1807 if num == int(rg.Start.LineNumber) {
1808 c0 = int(rg.Start.Column) - 1
1809 }
1810 if num == int(rg.End.LineNumber) {
1811 c1 = int(rg.End.Column) - 1
1812 }
1813 c0 = max(0, min(c0, len(runes)))
1814 c1 = max(0, min(c1, len(runes)))
1815 if c0 < c1 {
1816 intervals = append(intervals, [2]int{c0, c1})
1817 }
1818 }
1819 intervals = mergeIntervals(intervals)
1820
1821 out[i] = ChunkLine{
1822 Num: num,
1823 Spans: spanRunes(runes, intervals),
1824 Highlight: len(intervals) > 0,
1825 }
1826 }
1827 return out
1828}
1829
1830// FileNameSpans splits a filename into matched/unmatched spans using ranges.
1831// Filename ranges live on line 1; columns are clamped to rune bounds.
1832func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan {
1833 runes := []rune(name)
1834 var intervals [][2]int
1835 for _, rg := range ranges {
1836 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 {
1837 continue
1838 }
1839 c0 := max(0, min(int(rg.Start.Column)-1, len(runes)))
1840 c1 := max(0, min(int(rg.End.Column)-1, len(runes)))
1841 if c0 < c1 {
1842 intervals = append(intervals, [2]int{c0, c1})
1843 }
1844 }
1845 return spanRunes(runes, mergeIntervals(intervals))
1846}
1847
1848type CodeSearchParams struct {
1849 BaseParams
1850 FilterType string // "code"
1851 FilterQuery string
1852 Results []SearchResult
1853 Page pagination.Page
1854 HasMore bool
1855 ErrorMsg string
1856
1857 MatchCount int
1858 FileCount int
1859 TimeTaken time.Duration
1860}
1861
1862func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error {
1863 params.FilterType = "code"
1864 return p.execute("search/search", w, params)
1865}
1866
1867func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1868 return p.execute("timeline/home", w, params)
1869}
1870
1871type CommentBodyFragmentParams struct {
1872 Comment models.Comment
1873 Reactions map[models.ReactionKind]models.ReactionDisplayData
1874 UserReacted map[models.ReactionKind]bool
1875}
1876
1877func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1878 return p.executePlain("fragments/comment/commentBody", w, params)
1879}
1880
1881type PullCommentFragmentParams struct {
1882 LoggedInUser *oauth.MultiAccountUser
1883 Comment models.Comment
1884 Reactions map[models.ReactionKind]models.ReactionDisplayData
1885 UserReacted map[models.ReactionKind]bool
1886 HxSwapOob bool
1887}
1888
1889func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error {
1890 return p.executePlain("fragments/comment/pullComment", w, params)
1891}
1892
1893type CommentHeaderFragmentParams struct {
1894 Comment models.Comment
1895 Reactions map[models.ReactionKind]models.ReactionDisplayData
1896 UserReacted map[models.ReactionKind]bool
1897 HxSwapOob bool
1898}
1899
1900func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1901 return p.executePlain("fragments/comment/commentHeader", w, params)
1902}
1903
1904type EditCommentFragmentParams struct {
1905 Comment models.Comment
1906}
1907
1908func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1909 return p.executePlain("fragments/comment/edit", w, params)
1910}
1911
1912type ReplyCommentFragmentParams struct {
1913 BaseParams
1914}
1915
1916func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1917 return p.executePlain("fragments/comment/reply", w, params)
1918}
1919
1920type ReplyPlaceholderFragmentParams struct {
1921 BaseParams
1922}
1923
1924func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
1925 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
1926}
1927
1928func (p *Pages) Static() http.Handler {
1929 if p.dev {
1930 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1931 }
1932
1933 sub, err := fs.Sub(p.embedFS, "static")
1934 if err != nil {
1935 p.logger.Error("no static dir found? that's crazy", "err", err)
1936 panic(err)
1937 }
1938 // Custom handler to apply Cache-Control headers for font files
1939 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1940}
1941
1942func Cache(h http.Handler) http.Handler {
1943 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1944 path := strings.Split(r.URL.Path, "?")[0]
1945
1946 if strings.HasSuffix(path, ".css") {
1947 // on day for css files
1948 w.Header().Set("Cache-Control", "public, max-age=86400")
1949 } else {
1950 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1951 }
1952 h.ServeHTTP(w, r)
1953 })
1954}
1955
1956func (p *Pages) CssContentHash() string {
1957 cssFile, err := p.embedFS.Open("static/tw.css")
1958 if err != nil {
1959 slog.Debug("Error opening CSS file", "err", err)
1960 return ""
1961 }
1962 defer cssFile.Close()
1963
1964 hasher := sha256.New()
1965 if _, err := io.Copy(hasher, cssFile); err != nil {
1966 slog.Debug("Error hashing CSS file", "err", err)
1967 return ""
1968 }
1969
1970 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1971}
1972
1973func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
1974 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
1975}
1976
1977func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
1978 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
1979}
1980
1981func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
1982 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
1983}
1984
1985func (p *Pages) Error500(w io.Writer) error {
1986 return p.execute("errors/500", w, nil)
1987}
1988
1989func (p *Pages) Error404(w io.Writer) error {
1990 return p.execute("errors/404", w, nil)
1991}
1992
1993func (p *Pages) ErrorKnot404(w io.Writer) error {
1994 return p.execute("errors/knot404", w, nil)
1995}
1996
1997func (p *Pages) Error503(w io.Writer) error {
1998 return p.execute("errors/503", w, nil)
1999}