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/repoinfo"
29 "tangled.org/core/appview/pagination"
30 "tangled.org/core/idresolver"
31 "tangled.org/core/types"
32
33 "github.com/bluesky-social/indigo/atproto/identity"
34 "github.com/bluesky-social/indigo/atproto/syntax"
35 "github.com/go-git/go-git/v5/plumbing"
36 "github.com/sourcegraph/zoekt"
37)
38
39//go:embed templates/* static legal
40var Files embed.FS
41
42type baseParamsCtxKey struct{}
43
44type BaseParams struct {
45 LoggedInUser *oauth.MultiAccountUser
46 FocusParams FocusParams
47}
48
49type FocusParams struct {
50 Focusing bool
51 FocusLink string
52 FocusNotificationID int64
53 CurrentPath string // r.URL.Path, for off-focus detection in templates
54 FocusCount int // total unread focus-eligible items remaining
55}
56
57func (p *Pages) Resolver() *idresolver.Resolver {
58 return p.resolver
59}
60
61func BaseParamsIntoContext(ctx context.Context, bp BaseParams) context.Context {
62 return context.WithValue(ctx, baseParamsCtxKey{}, bp)
63}
64
65func BaseParamsFromContext(ctx context.Context) BaseParams {
66 bp, _ := ctx.Value(baseParamsCtxKey{}).(BaseParams)
67 return bp
68}
69
70type Pages struct {
71 mu sync.RWMutex
72 cache *TmplCache[string, *template.Template]
73
74 avatar config.AvatarConfig
75 pdsCfg config.PdsConfig
76 resolver *idresolver.Resolver
77 db *db.DB
78 rdb *cache.Cache
79 dev bool
80 embedFS fs.FS
81 templateDir string // Path to templates on disk for dev mode
82 rctx *markup.RenderContext
83 logger *slog.Logger
84}
85
86func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages {
87 // initialized with safe defaults, can be overridden per use
88 rctx := &markup.RenderContext{
89 IsDev: config.Core.Dev,
90 Hostname: config.Core.AppviewHost,
91 CamoUrl: config.Camo.Host,
92 CamoSecret: config.Camo.SharedSecret,
93 Sanitizer: markup.NewSanitizer(),
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 := rctx.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 := rctx.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 := rctx.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 RepoLogParams struct {
934 BaseParams
935 RepoInfo repoinfo.RepoInfo
936 TagMap map[string][]string
937 Active string
938 EmailToDid map[string]string
939 VerifiedCommits commitverify.VerifiedCommits
940 Pipelines map[string]models.Pipeline
941
942 types.RepoLogResponse
943}
944
945func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
946 params.Active = "overview"
947 return p.executeRepo("repo/log", w, params)
948}
949
950type RepoCommitParams struct {
951 BaseParams
952 RepoInfo repoinfo.RepoInfo
953 Active string
954 EmailToDid map[string]string
955 Pipeline *models.Pipeline
956 DiffOpts types.DiffOpts
957
958 // singular because it's always going to be just one
959 VerifiedCommit commitverify.VerifiedCommits
960
961 types.RepoCommitResponse
962}
963
964func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
965 params.Active = "overview"
966 return p.executeRepo("repo/commit", w, params)
967}
968
969type RepoTreeParams struct {
970 BaseParams
971 RepoInfo repoinfo.RepoInfo
972 Active string
973 BreadCrumbs [][]string
974 Path string
975 Raw bool
976 HTMLReadme template.HTML
977 EmailToDid map[string]string
978 LastCommitInfo *types.LastCommitInfo
979 Ref string
980 Parent string
981 DotDot string
982 Files []types.NiceTree
983 ReadmeFileName string
984 Readme string
985}
986
987type RepoTreeStats struct {
988 NumFolders uint64
989 NumFiles uint64
990}
991
992func (r RepoTreeParams) TreeStats() RepoTreeStats {
993 numFolders, numFiles := 0, 0
994 for _, f := range r.Files {
995 if !f.IsFile() {
996 numFolders += 1
997 } else if f.IsFile() {
998 numFiles += 1
999 }
1000 }
1001
1002 return RepoTreeStats{
1003 NumFolders: uint64(numFolders),
1004 NumFiles: uint64(numFiles),
1005 }
1006}
1007
1008func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
1009 params.Active = "overview"
1010
1011 rctx := p.rctx.Clone()
1012 rctx.RepoInfo = params.RepoInfo
1013 rctx.RepoInfo.Ref = params.Ref
1014 rctx.RendererType = markup.RendererTypeRepoMarkdown
1015
1016 if params.ReadmeFileName != "" {
1017 switch markup.GetFormat(params.ReadmeFileName) {
1018 case markup.FormatMarkdown:
1019 params.Raw = false
1020 htmlString := rctx.RenderMarkdown(params.Readme)
1021 sanitized := rctx.SanitizeDefault(htmlString)
1022 params.HTMLReadme = template.HTML(sanitized)
1023 default:
1024 params.Raw = true
1025 }
1026 }
1027
1028 return p.executeRepo("repo/tree", w, params)
1029}
1030
1031type RepoBranchesParams struct {
1032 BaseParams
1033 RepoInfo repoinfo.RepoInfo
1034 Active string
1035 types.RepoBranchesResponse
1036}
1037
1038func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
1039 params.Active = "overview"
1040 return p.executeRepo("repo/branches", w, params)
1041}
1042
1043type RepoTagsParams struct {
1044 BaseParams
1045 RepoInfo repoinfo.RepoInfo
1046 Active string
1047 types.RepoTagsResponse
1048 ArtifactMap map[plumbing.Hash][]models.Artifact
1049 DanglingArtifacts []models.Artifact
1050}
1051
1052func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
1053 params.Active = "overview"
1054 return p.executeRepo("repo/tags", w, params)
1055}
1056
1057type RepoTagParams struct {
1058 BaseParams
1059 RepoInfo repoinfo.RepoInfo
1060 Active string
1061 types.RepoTagResponse
1062 ArtifactMap map[plumbing.Hash][]models.Artifact
1063 DanglingArtifacts []models.Artifact
1064}
1065
1066func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error {
1067 params.Active = "overview"
1068 return p.executeRepo("repo/tag", w, params)
1069}
1070
1071type RepoArtifactParams struct {
1072 BaseParams
1073 RepoInfo repoinfo.RepoInfo
1074 Artifact models.Artifact
1075}
1076
1077func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
1078 return p.executePlain("repo/fragments/artifact", w, params)
1079}
1080
1081type RepoBlobParams struct {
1082 BaseParams
1083 RepoInfo repoinfo.RepoInfo
1084 Active string // always "overview"
1085 BreadCrumbs [][]string
1086 BlobView models.BlobView // TODO: expose this struct
1087 ShowRendered bool
1088 EmailToDid map[string]string
1089 LastCommitInfo *types.LastCommitInfo
1090 Ref string
1091 Path string
1092}
1093
1094func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
1095 params.Active = "overview"
1096 return p.executeRepo("repo/blob", w, params)
1097}
1098
1099type Collaborator struct {
1100 Did string
1101 Role string
1102}
1103
1104type RepoSettingsParams struct {
1105 BaseParams
1106 RepoInfo repoinfo.RepoInfo
1107 Collaborators []Collaborator
1108 Active string
1109 Branches []types.Branch
1110 Spindles []string
1111 CurrentSpindle string
1112 Secrets []*tangled.RepoListSecrets_Secret
1113
1114 // TODO: use repoinfo.roles
1115 IsCollaboratorInviteAllowed bool
1116}
1117
1118func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
1119 params.Active = "settings"
1120 return p.executeRepo("repo/settings", w, params)
1121}
1122
1123type RepoGeneralSettingsParams struct {
1124 BaseParams
1125 RepoInfo repoinfo.RepoInfo
1126 Labels []models.LabelDefinition
1127 DefaultLabels []models.LabelDefinition
1128 SubscribedLabels map[string]struct{}
1129 ShouldSubscribeAll bool
1130 Active string
1131 Tab string
1132 Branches []types.Branch
1133}
1134
1135func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
1136 params.Active = "settings"
1137 params.Tab = "general"
1138 return p.executeRepo("repo/settings/general", w, params)
1139}
1140
1141type RepoAccessSettingsParams struct {
1142 BaseParams
1143 RepoInfo repoinfo.RepoInfo
1144 Active string
1145 Tab string
1146 Collaborators []Collaborator
1147 CanRemoveCollaborator bool
1148}
1149
1150func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
1151 params.Active = "settings"
1152 params.Tab = "access"
1153 return p.executeRepo("repo/settings/access", w, params)
1154}
1155
1156type RepoPipelineSettingsParams struct {
1157 BaseParams
1158 RepoInfo repoinfo.RepoInfo
1159 Active string
1160 Tab string
1161 Spindles []string
1162 CurrentSpindle string
1163 Secrets []map[string]any
1164}
1165
1166func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
1167 params.Active = "settings"
1168 params.Tab = "pipelines"
1169 return p.executeRepo("repo/settings/pipelines", w, params)
1170}
1171
1172type RepoWebhooksSettingsParams struct {
1173 BaseParams
1174 RepoInfo repoinfo.RepoInfo
1175 Active string
1176 Tab string
1177 Webhooks []models.Webhook
1178 WebhookDeliveries map[int64][]models.WebhookDelivery
1179}
1180
1181func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error {
1182 params.Active = "settings"
1183 params.Tab = "hooks"
1184 return p.executeRepo("repo/settings/hooks", w, params)
1185}
1186
1187type WebhookDeliveriesListParams struct {
1188 BaseParams
1189 RepoInfo repoinfo.RepoInfo
1190 Webhook *models.Webhook
1191 Deliveries []models.WebhookDelivery
1192}
1193
1194func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error {
1195 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries")
1196 if err != nil {
1197 return err
1198 }
1199 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params)
1200}
1201
1202type RepoSiteSettingsParams struct {
1203 BaseParams
1204 RepoInfo repoinfo.RepoInfo
1205 Active string
1206 Tab string
1207 Branches []types.Branch
1208 SiteConfig *models.RepoSite
1209 OwnerClaim *models.DomainClaim
1210 Deploys []models.SiteDeploy
1211 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or ""
1212}
1213
1214func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error {
1215 params.Active = "settings"
1216 params.Tab = "sites"
1217 return p.executeRepo("repo/settings/sites", w, params)
1218}
1219
1220type RepoIssuesParams struct {
1221 BaseParams
1222 RepoInfo repoinfo.RepoInfo
1223 Active string
1224 Issues []models.Issue
1225 IssueCount int
1226 LabelDefs map[string]*models.LabelDefinition
1227 Page pagination.Page
1228 FilterState string
1229 FilterQuery string
1230 BaseFilterQuery string
1231 VouchRelationships map[syntax.DID]*models.VouchRelationship
1232}
1233
1234func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
1235 params.Active = "issues"
1236 return p.executeRepo("repo/issues/issues", w, params)
1237}
1238
1239type RepoSingleIssueParams struct {
1240 BaseParams
1241 RepoInfo repoinfo.RepoInfo
1242 Active string
1243 Issue *models.Issue
1244 CommentList []models.CommentListItem
1245 Backlinks []models.RichReferenceLink
1246 LabelDefs map[string]*models.LabelDefinition
1247
1248 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1249 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1250 VouchRelationships map[syntax.DID]*models.VouchRelationship
1251}
1252
1253func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
1254 params.Active = "issues"
1255 return p.executeRepo("repo/issues/issue", w, params)
1256}
1257
1258type EditIssueParams struct {
1259 BaseParams
1260 RepoInfo repoinfo.RepoInfo
1261 Issue *models.Issue
1262 Action string
1263}
1264
1265func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error {
1266 params.Action = "edit"
1267 return p.executePlain("repo/issues/fragments/putIssue", w, params)
1268}
1269
1270type ThreadReactionFragmentParams struct {
1271 Kind models.ReactionKind
1272 Count int
1273 Users []string
1274 IsReacted bool
1275 CommentRkey string
1276 SubjectUri string
1277}
1278
1279func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error {
1280 return p.executePlain("repo/fragments/reaction", w, params)
1281}
1282
1283type RepoNewIssueParams struct {
1284 BaseParams
1285 RepoInfo repoinfo.RepoInfo
1286 Issue *models.Issue // existing issue if any -- passed when editing
1287 Active string
1288 Action string
1289}
1290
1291func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
1292 params.Active = "issues"
1293 params.Action = "create"
1294 return p.executeRepo("repo/issues/new", w, params)
1295}
1296
1297type StackedDiff struct {
1298 Diff *types.NiceDiff
1299 Opts types.DiffOpts
1300}
1301
1302type RepoNewPullParams struct {
1303 BaseParams
1304 RepoInfo repoinfo.RepoInfo
1305 Branches []types.Branch
1306 SourceBranches []types.Branch
1307 ForkBranches []types.Branch
1308 Forks []models.Repo
1309 Source Source
1310 SourceBranch string
1311 TargetBranch string
1312 Fork string
1313 Patch string
1314 Title string
1315 Body string
1316 TitleDirty bool
1317 BodyDirty bool
1318 IsStacked bool
1319 Comparison *types.RepoFormatPatchResponse
1320 Diff *types.NiceDiff
1321 DiffOpts types.DiffOpts
1322 StackedDiffs []StackedDiff
1323 MergeCheck *types.MergeCheckResponse
1324 StackTitles map[string]string
1325 StackBodies map[string]string
1326 PrefillError string
1327 Active string
1328 LabelDefs map[string]*models.LabelDefinition
1329 LabelState models.LabelState
1330 StackLabelStates map[string]models.LabelState
1331}
1332
1333func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
1334 params.Active = "pulls"
1335 return p.executeRepo("repo/pulls/new", w, params)
1336}
1337
1338func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error {
1339 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params)
1340}
1341
1342func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error {
1343 return p.executePlain("fragments/markdownPreview", w, body)
1344}
1345
1346type RepoPullsParams struct {
1347 BaseParams
1348 RepoInfo repoinfo.RepoInfo
1349 Pulls []*models.Pull
1350 Active string
1351 FilterState string
1352 FilterQuery string
1353 BaseFilterQuery string
1354 Stacks []models.Stack
1355 Pipelines map[string]models.Pipeline
1356 LabelDefs map[string]*models.LabelDefinition
1357 Page pagination.Page
1358 PullCount int
1359 VouchRelationships map[syntax.DID]*models.VouchRelationship
1360}
1361
1362func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
1363 params.Active = "pulls"
1364 return p.executeRepo("repo/pulls/pulls", w, params)
1365}
1366
1367type ResubmitResult uint64
1368
1369const (
1370 ShouldResubmit ResubmitResult = iota
1371 ShouldNotResubmit
1372 Unknown
1373)
1374
1375func (r ResubmitResult) Yes() bool {
1376 return r == ShouldResubmit
1377}
1378func (r ResubmitResult) No() bool {
1379 return r == ShouldNotResubmit
1380}
1381func (r ResubmitResult) Unknown() bool {
1382 return r == Unknown
1383}
1384
1385type RepoSinglePullParams struct {
1386 BaseParams
1387 RepoInfo repoinfo.RepoInfo
1388 Active string
1389 Pull *models.Pull
1390 Stack models.Stack
1391 Backlinks []models.RichReferenceLink
1392 BranchDeleteStatus *models.BranchDeleteStatus
1393 MergeCheck types.MergeCheckResponse
1394 ResubmitCheck ResubmitResult
1395 Pipelines map[string]models.Pipeline
1396 Diff types.DiffRenderer
1397 DiffOpts types.DiffOpts
1398 ActiveRound int
1399 IsInterdiff bool
1400
1401 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1402 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1403
1404 LabelDefs map[string]*models.LabelDefinition
1405 VouchRelationships map[syntax.DID]*models.VouchRelationship
1406 VouchSkips map[syntax.DID]bool
1407}
1408
1409func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1410 params.Active = "pulls"
1411 return p.executeRepo("repo/pulls/pull", w, params)
1412}
1413
1414type PullResubmitParams struct {
1415 BaseParams
1416 RepoInfo repoinfo.RepoInfo
1417 Pull *models.Pull
1418 SubmissionId int
1419}
1420
1421func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1422 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1423}
1424
1425type PullActionsParams struct {
1426 BaseParams
1427 RepoInfo repoinfo.RepoInfo
1428 Pull *models.Pull
1429 RoundNumber int
1430 MergeCheck types.MergeCheckResponse
1431 ResubmitCheck ResubmitResult
1432 BranchDeleteStatus *models.BranchDeleteStatus
1433 Stack models.Stack
1434
1435 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1436 // that fetches the real, checked fragment
1437 Loading bool
1438}
1439
1440func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1441 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1442}
1443
1444type PullNewCommentParams struct {
1445 BaseParams
1446 RepoInfo repoinfo.RepoInfo
1447 Pull *models.Pull
1448 RoundNumber int
1449}
1450
1451func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
1452 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
1453}
1454
1455type RepoCompareParams struct {
1456 BaseParams
1457 RepoInfo repoinfo.RepoInfo
1458 Forks []models.Repo
1459 Branches []types.Branch
1460 Tags []*types.TagReference
1461 Base string
1462 Head string
1463 Diff *types.NiceDiff
1464 DiffOpts types.DiffOpts
1465
1466 Active string
1467}
1468
1469func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1470 params.Active = "overview"
1471 return p.executeRepo("repo/compare/compare", w, params)
1472}
1473
1474type RepoCompareNewParams struct {
1475 BaseParams
1476 RepoInfo repoinfo.RepoInfo
1477 Forks []models.Repo
1478 Branches []types.Branch
1479 Tags []*types.TagReference
1480 Base string
1481 Head string
1482
1483 Active string
1484}
1485
1486func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1487 params.Active = "overview"
1488 return p.executeRepo("repo/compare/new", w, params)
1489}
1490
1491type RepoCompareAllowPullParams struct {
1492 BaseParams
1493 RepoInfo repoinfo.RepoInfo
1494 Base string
1495 Head string
1496}
1497
1498func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1499 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1500}
1501
1502type RepoCompareDiffFragmentParams struct {
1503 Diff types.NiceDiff
1504 DiffOpts types.DiffOpts
1505}
1506
1507func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1508 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1509}
1510
1511type LabelPanelParams struct {
1512 BaseParams
1513 RepoInfo repoinfo.RepoInfo
1514 Defs map[string]*models.LabelDefinition
1515 Subject string
1516 State models.LabelState
1517}
1518
1519func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1520 return p.executePlain("repo/fragments/labelPanel", w, params)
1521}
1522
1523type EditLabelPanelParams struct {
1524 BaseParams
1525 RepoInfo repoinfo.RepoInfo
1526 Defs map[string]*models.LabelDefinition
1527 Subject string
1528 State models.LabelState
1529 Prefix string
1530}
1531
1532func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1533 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1534}
1535
1536type RepoStarsParams struct {
1537 BaseParams
1538 RepoInfo repoinfo.RepoInfo
1539 Active string
1540 Starrers []models.Star
1541 Page pagination.Page
1542 TotalCount int
1543}
1544
1545func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1546 params.Active = "overview"
1547 return p.executeRepo("repo/stars", w, params)
1548}
1549
1550type RepoForksParams struct {
1551 BaseParams
1552 RepoInfo repoinfo.RepoInfo
1553 Active string
1554 Forks []models.Repo
1555 Page pagination.Page
1556 TotalCount int
1557}
1558
1559func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1560 params.Active = "overview"
1561 return p.executeRepo("repo/forks", w, params)
1562}
1563
1564type PipelinesParams struct {
1565 BaseParams
1566 RepoInfo repoinfo.RepoInfo
1567 Pipelines []models.Pipeline
1568 Active string
1569 FilterKind string
1570 Total int64
1571}
1572
1573func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1574 params.Active = "pipelines"
1575 return p.executeRepo("repo/pipelines/pipelines", w, params)
1576}
1577
1578type LogBlockParams struct {
1579 Id int
1580 Name string
1581 Command string
1582 Collapsed bool
1583 StartTime time.Time
1584}
1585
1586func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1587 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1588}
1589
1590type LogBlockEndParams struct {
1591 Id int
1592 StartTime time.Time
1593 EndTime time.Time
1594}
1595
1596func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1597 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1598}
1599
1600type LogLineParams struct {
1601 Id int
1602 Content template.HTML
1603}
1604
1605func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1606 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1607}
1608
1609type WorkflowSymbolOOBParams struct {
1610 Name string
1611 Statuses models.WorkflowStatus
1612}
1613
1614func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1615 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1616}
1617
1618type WorkflowParams struct {
1619 BaseParams
1620 RepoInfo repoinfo.RepoInfo
1621 Pipeline models.Pipeline
1622 Workflow string
1623 LogUrl string
1624 Active string
1625}
1626
1627func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1628 params.Active = "pipelines"
1629 return p.executeRepo("repo/pipelines/workflow", w, params)
1630}
1631
1632type PutStringParams struct {
1633 BaseParams
1634 Action string
1635
1636 // this is supplied in the case of editing an existing string
1637 String models.String
1638}
1639
1640func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1641 return p.execute("strings/put", w, params)
1642}
1643
1644type StringsDashboardParams struct {
1645 BaseParams
1646 Card ProfileCard
1647 Strings []models.String
1648}
1649
1650func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1651 return p.execute("strings/dashboard", w, params)
1652}
1653
1654type StringTimelineParams struct {
1655 BaseParams
1656 Strings []models.String
1657}
1658
1659func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1660 return p.execute("strings/timeline", w, params)
1661}
1662
1663type SingleStringParams struct {
1664 BaseParams
1665 ShowRendered bool
1666 RenderToggle bool
1667 RenderedContents template.HTML
1668 String *models.String
1669 Stats models.StringStats
1670 IsStarred bool
1671 StarCount int
1672 Owner identity.Identity
1673 CommentList []models.CommentListItem
1674
1675 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1676 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1677 VouchRelationships map[syntax.DID]*models.VouchRelationship
1678}
1679
1680func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1681 return p.execute("strings/string", w, params)
1682}
1683
1684type SearchReposParams struct {
1685 BaseParams
1686 FilterType string // "repo" | "code"
1687 Repos []SearchResult
1688 Page pagination.Page
1689 ResultCount int
1690 FilterQuery string
1691 SortParam string
1692 TimeTaken time.Duration
1693 DocCount int64
1694 ErrorMsg string
1695}
1696
1697func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1698 params.FilterType = "repo"
1699 return p.execute("search/search", w, params)
1700}
1701
1702type SearchQuickParams struct {
1703 Repos []models.Repo
1704 Query string
1705 Total int
1706}
1707
1708func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1709 return p.executePlain("search/fragments/quick", w, params)
1710}
1711
1712func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1713 tpl, err := p.parse("search/fragments/quick")
1714 if err != nil {
1715 return err
1716 }
1717 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1718}
1719
1720type SearchResult struct {
1721 RepoDID syntax.DID
1722 Repo *models.Repo
1723 FilePath string
1724 Branches []string
1725 Commit string
1726 Language string
1727
1728 File *CodeSearchResult_File // filename match
1729 Chunks CodeSearchResult_Chunks // content matches
1730}
1731
1732// CodeSearchResult_Chunk is a content match with its lines pre-rendered.
1733type CodeSearchResult_Chunk struct {
1734 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges
1735 MatchCount int // number of match ranges in this chunk
1736}
1737
1738type CodeSearchResult_Chunks []CodeSearchResult_Chunk
1739
1740func (cs CodeSearchResult_Chunks) MatchCount() int {
1741 count := 0
1742 for _, c := range cs {
1743 count += c.MatchCount
1744 }
1745 return count
1746}
1747
1748type CodeSearchResult_File struct {
1749 NameSpans []ChunkSpan // precomputed from FilePath/Ranges
1750}
1751
1752type ChunkSpan struct {
1753 Text string
1754 Match bool
1755}
1756
1757type ChunkLine struct {
1758 Num int
1759 Spans []ChunkSpan
1760 Highlight bool
1761}
1762
1763// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each
1764// line into matched/unmatched spans using ranges. startLine is the 1-based line
1765// number of the first line.
1766func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine {
1767 if startLine < 1 {
1768 startLine = 1
1769 }
1770 // trim a single trailing newline so we don't emit a spurious empty line
1771 content = strings.TrimSuffix(content, "\n")
1772 lines := strings.Split(content, "\n")
1773 out := make([]ChunkLine, len(lines))
1774 for i, text := range lines {
1775 num := startLine + i
1776 runes := []rune(text)
1777
1778 // collect matched rune intervals [c0,c1) for this line
1779 var intervals [][2]int
1780 for _, rg := range ranges {
1781 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) {
1782 continue
1783 }
1784 c0, c1 := 0, len(runes)
1785 if num == int(rg.Start.LineNumber) {
1786 c0 = int(rg.Start.Column) - 1
1787 }
1788 if num == int(rg.End.LineNumber) {
1789 c1 = int(rg.End.Column) - 1
1790 }
1791 c0 = max(0, min(c0, len(runes)))
1792 c1 = max(0, min(c1, len(runes)))
1793 if c0 < c1 {
1794 intervals = append(intervals, [2]int{c0, c1})
1795 }
1796 }
1797 intervals = mergeIntervals(intervals)
1798
1799 out[i] = ChunkLine{
1800 Num: num,
1801 Spans: spanRunes(runes, intervals),
1802 Highlight: len(intervals) > 0,
1803 }
1804 }
1805 return out
1806}
1807
1808// FileNameSpans splits a filename into matched/unmatched spans using ranges.
1809// Filename ranges live on line 1; columns are clamped to rune bounds.
1810func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan {
1811 runes := []rune(name)
1812 var intervals [][2]int
1813 for _, rg := range ranges {
1814 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 {
1815 continue
1816 }
1817 c0 := max(0, min(int(rg.Start.Column)-1, len(runes)))
1818 c1 := max(0, min(int(rg.End.Column)-1, len(runes)))
1819 if c0 < c1 {
1820 intervals = append(intervals, [2]int{c0, c1})
1821 }
1822 }
1823 return spanRunes(runes, mergeIntervals(intervals))
1824}
1825
1826type CodeSearchParams struct {
1827 BaseParams
1828 FilterType string // "code"
1829 FilterQuery string
1830 Results []SearchResult
1831 Page pagination.Page
1832 HasMore bool
1833 ErrorMsg string
1834
1835 MatchCount int
1836 FileCount int
1837 TimeTaken time.Duration
1838}
1839
1840func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error {
1841 params.FilterType = "code"
1842 return p.execute("search/search", w, params)
1843}
1844
1845func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1846 return p.execute("timeline/home", w, params)
1847}
1848
1849type CommentBodyFragmentParams struct {
1850 Comment models.Comment
1851 Reactions map[models.ReactionKind]models.ReactionDisplayData
1852 UserReacted map[models.ReactionKind]bool
1853}
1854
1855func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1856 return p.executePlain("fragments/comment/commentBody", w, params)
1857}
1858
1859type CommentHeaderFragmentParams struct {
1860 Comment models.Comment
1861 Reactions map[models.ReactionKind]models.ReactionDisplayData
1862 UserReacted map[models.ReactionKind]bool
1863 HxSwapOob bool
1864}
1865
1866func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1867 return p.executePlain("fragments/comment/commentHeader", w, params)
1868}
1869
1870type EditCommentFragmentParams struct {
1871 Comment models.Comment
1872}
1873
1874func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1875 return p.executePlain("fragments/comment/edit", w, params)
1876}
1877
1878type ReplyCommentFragmentParams struct {
1879 BaseParams
1880}
1881
1882func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1883 return p.executePlain("fragments/comment/reply", w, params)
1884}
1885
1886type ReplyPlaceholderFragmentParams struct {
1887 BaseParams
1888}
1889
1890func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
1891 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
1892}
1893
1894func (p *Pages) Static() http.Handler {
1895 if p.dev {
1896 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1897 }
1898
1899 sub, err := fs.Sub(p.embedFS, "static")
1900 if err != nil {
1901 p.logger.Error("no static dir found? that's crazy", "err", err)
1902 panic(err)
1903 }
1904 // Custom handler to apply Cache-Control headers for font files
1905 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1906}
1907
1908func Cache(h http.Handler) http.Handler {
1909 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1910 path := strings.Split(r.URL.Path, "?")[0]
1911
1912 if strings.HasSuffix(path, ".css") {
1913 // on day for css files
1914 w.Header().Set("Cache-Control", "public, max-age=86400")
1915 } else {
1916 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1917 }
1918 h.ServeHTTP(w, r)
1919 })
1920}
1921
1922func (p *Pages) CssContentHash() string {
1923 cssFile, err := p.embedFS.Open("static/tw.css")
1924 if err != nil {
1925 slog.Debug("Error opening CSS file", "err", err)
1926 return ""
1927 }
1928 defer cssFile.Close()
1929
1930 hasher := sha256.New()
1931 if _, err := io.Copy(hasher, cssFile); err != nil {
1932 slog.Debug("Error hashing CSS file", "err", err)
1933 return ""
1934 }
1935
1936 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1937}
1938
1939func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
1940 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
1941}
1942
1943func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
1944 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
1945}
1946
1947func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
1948 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
1949}
1950
1951func (p *Pages) Error500(w io.Writer) error {
1952 return p.execute("errors/500", w, nil)
1953}
1954
1955func (p *Pages) Error404(w io.Writer) error {
1956 return p.execute("errors/404", w, nil)
1957}
1958
1959func (p *Pages) ErrorKnot404(w io.Writer) error {
1960 return p.execute("errors/knot404", w, nil)
1961}
1962
1963func (p *Pages) Error503(w io.Writer) error {
1964 return p.execute("errors/503", w, nil)
1965}