forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package pages 2 3import ( 4 "bytes" 5 "embed" 6 "fmt" 7 "html/template" 8 "io" 9 "io/fs" 10 "log" 11 "net/http" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/alecthomas/chroma/v2" 17 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 18 "github.com/alecthomas/chroma/v2/lexers" 19 "github.com/alecthomas/chroma/v2/styles" 20 "github.com/sotangled/tangled/appview/auth" 21 "github.com/sotangled/tangled/appview/db" 22 "github.com/sotangled/tangled/types" 23) 24 25//go:embed templates/* static/* 26var files embed.FS 27 28type Pages struct { 29 t map[string]*template.Template 30} 31 32func NewPages() *Pages { 33 templates := make(map[string]*template.Template) 34 35 // Walk through embedded templates directory and parse all .html files 36 err := fs.WalkDir(files, "templates", func(path string, d fs.DirEntry, err error) error { 37 if err != nil { 38 return err 39 } 40 41 if !d.IsDir() && strings.HasSuffix(path, ".html") { 42 name := strings.TrimPrefix(path, "templates/") 43 name = strings.TrimSuffix(name, ".html") 44 45 if !strings.HasPrefix(path, "templates/layouts/") { 46 // Add the page template on top of the base 47 tmpl, err := template.New(name). 48 Funcs(funcMap()). 49 ParseFS(files, "templates/layouts/*.html", path) 50 if err != nil { 51 return fmt.Errorf("setting up template: %w", err) 52 } 53 54 templates[name] = tmpl 55 log.Printf("loaded template: %s", name) 56 } 57 58 return nil 59 } 60 return nil 61 }) 62 if err != nil { 63 log.Fatalf("walking template dir: %v", err) 64 } 65 66 log.Printf("total templates loaded: %d", len(templates)) 67 68 return &Pages{ 69 t: templates, 70 } 71} 72 73type LoginParams struct { 74} 75 76func (p *Pages) execute(name string, w io.Writer, params any) error { 77 return p.t[name].ExecuteTemplate(w, "layouts/base", params) 78} 79 80func (p *Pages) executePlain(name string, w io.Writer, params any) error { 81 return p.t[name].Execute(w, params) 82} 83 84func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 85 return p.t[name].ExecuteTemplate(w, "layouts/repobase", params) 86} 87 88func (p *Pages) Login(w io.Writer, params LoginParams) error { 89 return p.executePlain("user/login", w, params) 90} 91 92type TimelineParams struct { 93 LoggedInUser *auth.User 94 Timeline []db.TimelineEvent 95 DidHandleMap map[string]string 96} 97 98func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 99 return p.execute("timeline", w, params) 100} 101 102type SettingsParams struct { 103 LoggedInUser *auth.User 104 PubKeys []db.PublicKey 105} 106 107func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 108 return p.execute("settings/keys", w, params) 109} 110 111type KnotsParams struct { 112 LoggedInUser *auth.User 113 Registrations []db.Registration 114} 115 116func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 117 return p.execute("knots", w, params) 118} 119 120type KnotParams struct { 121 LoggedInUser *auth.User 122 Registration *db.Registration 123 Members []string 124 IsOwner bool 125} 126 127func (p *Pages) Knot(w io.Writer, params KnotParams) error { 128 return p.execute("knot", w, params) 129} 130 131type NewRepoParams struct { 132 LoggedInUser *auth.User 133 Knots []string 134} 135 136func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 137 return p.execute("repo/new", w, params) 138} 139 140type ProfilePageParams struct { 141 LoggedInUser *auth.User 142 UserDid string 143 UserHandle string 144 Repos []db.Repo 145 CollaboratingRepos []db.Repo 146 ProfileStats ProfileStats 147 FollowStatus db.FollowStatus 148 DidHandleMap map[string]string 149} 150 151type ProfileStats struct { 152 Followers int 153 Following int 154} 155 156func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 157 return p.execute("user/profile", w, params) 158} 159 160type RepoInfo struct { 161 Name string 162 OwnerDid string 163 OwnerHandle string 164 Description string 165 SettingsAllowed bool 166} 167 168func (r RepoInfo) OwnerWithAt() string { 169 if r.OwnerHandle != "" { 170 return fmt.Sprintf("@%s", r.OwnerHandle) 171 } else { 172 return r.OwnerDid 173 } 174} 175 176func (r RepoInfo) FullName() string { 177 return path.Join(r.OwnerWithAt(), r.Name) 178} 179 180func (r RepoInfo) GetTabs() [][]string { 181 tabs := [][]string{ 182 {"overview", "/"}, 183 {"issues", "/issues"}, 184 {"pulls", "/pulls"}, 185 } 186 187 if r.SettingsAllowed { 188 tabs = append(tabs, []string{"settings", "/settings"}) 189 } 190 191 return tabs 192} 193 194type RepoIndexParams struct { 195 LoggedInUser *auth.User 196 RepoInfo RepoInfo 197 Active string 198 TagMap map[string][]string 199 types.RepoIndexResponse 200} 201 202func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 203 params.Active = "overview" 204 if params.IsEmpty { 205 return p.executeRepo("repo/empty", w, params) 206 } 207 return p.executeRepo("repo/index", w, params) 208} 209 210type RepoLogParams struct { 211 LoggedInUser *auth.User 212 RepoInfo RepoInfo 213 types.RepoLogResponse 214 Active string 215} 216 217func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 218 params.Active = "overview" 219 return p.execute("repo/log", w, params) 220} 221 222type RepoCommitParams struct { 223 LoggedInUser *auth.User 224 RepoInfo RepoInfo 225 Active string 226 types.RepoCommitResponse 227} 228 229func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 230 params.Active = "overview" 231 return p.executeRepo("repo/commit", w, params) 232} 233 234type RepoTreeParams struct { 235 LoggedInUser *auth.User 236 RepoInfo RepoInfo 237 Active string 238 BreadCrumbs [][]string 239 BaseTreeLink string 240 BaseBlobLink string 241 types.RepoTreeResponse 242} 243 244type RepoTreeStats struct { 245 NumFolders uint64 246 NumFiles uint64 247} 248 249func (r RepoTreeParams) TreeStats() RepoTreeStats { 250 numFolders, numFiles := 0, 0 251 for _, f := range r.Files { 252 if !f.IsFile { 253 numFolders += 1 254 } else if f.IsFile { 255 numFiles += 1 256 } 257 } 258 259 return RepoTreeStats{ 260 NumFolders: uint64(numFolders), 261 NumFiles: uint64(numFiles), 262 } 263} 264 265func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 266 params.Active = "overview" 267 return p.execute("repo/tree", w, params) 268} 269 270type RepoBranchesParams struct { 271 LoggedInUser *auth.User 272 RepoInfo RepoInfo 273 types.RepoBranchesResponse 274} 275 276func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 277 return p.executeRepo("repo/branches", w, params) 278} 279 280type RepoTagsParams struct { 281 LoggedInUser *auth.User 282 RepoInfo RepoInfo 283 types.RepoTagsResponse 284} 285 286func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 287 return p.executeRepo("repo/tags", w, params) 288} 289 290type RepoBlobParams struct { 291 LoggedInUser *auth.User 292 RepoInfo RepoInfo 293 Active string 294 BreadCrumbs [][]string 295 types.RepoBlobResponse 296} 297 298func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 299 style := styles.Get("bw") 300 b := style.Builder() 301 b.Add(chroma.LiteralString, "noitalic") 302 style, _ = b.Build() 303 304 if params.Lines < 5000 { 305 c := params.Contents 306 formatter := chromahtml.New( 307 chromahtml.InlineCode(true), 308 chromahtml.WithLineNumbers(true), 309 chromahtml.WithLinkableLineNumbers(true, "L"), 310 chromahtml.Standalone(false), 311 ) 312 313 lexer := lexers.Get(filepath.Base(params.Path)) 314 if lexer == nil { 315 lexer = lexers.Fallback 316 } 317 318 iterator, err := lexer.Tokenise(nil, c) 319 if err != nil { 320 return fmt.Errorf("chroma tokenize: %w", err) 321 } 322 323 var code bytes.Buffer 324 err = formatter.Format(&code, style, iterator) 325 if err != nil { 326 return fmt.Errorf("chroma format: %w", err) 327 } 328 329 params.Contents = code.String() 330 } 331 332 params.Active = "overview" 333 return p.executeRepo("repo/blob", w, params) 334} 335 336type Collaborator struct { 337 Did string 338 Handle string 339 Role string 340} 341 342type RepoSettingsParams struct { 343 LoggedInUser *auth.User 344 RepoInfo RepoInfo 345 Collaborators []Collaborator 346 Active string 347 IsCollaboratorInviteAllowed bool 348} 349 350func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 351 params.Active = "settings" 352 return p.executeRepo("repo/settings", w, params) 353} 354 355type RepoIssuesParams struct { 356 LoggedInUser *auth.User 357 RepoInfo RepoInfo 358 Active string 359 Issues []db.Issue 360 DidHandleMap map[string]string 361} 362 363func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 364 params.Active = "issues" 365 return p.executeRepo("repo/issues/issues", w, params) 366} 367 368type RepoSingleIssueParams struct { 369 LoggedInUser *auth.User 370 RepoInfo RepoInfo 371 Active string 372 Issue db.Issue 373 Comments []db.Comment 374 IssueOwnerHandle string 375 DidHandleMap map[string]string 376 377 State string 378} 379 380func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 381 params.Active = "issues" 382 if params.Issue.Open { 383 params.State = "open" 384 } else { 385 params.State = "closed" 386 } 387 return p.execute("repo/issues/issue", w, params) 388} 389 390type RepoNewIssueParams struct { 391 LoggedInUser *auth.User 392 RepoInfo RepoInfo 393 Active string 394} 395 396func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 397 params.Active = "issues" 398 return p.executeRepo("repo/issues/new", w, params) 399} 400 401type RepoPullsParams struct { 402 LoggedInUser *auth.User 403 RepoInfo RepoInfo 404 Active string 405} 406 407func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 408 params.Active = "pulls" 409 return p.executeRepo("repo/pulls/pulls", w, params) 410} 411 412func (p *Pages) Static() http.Handler { 413 sub, err := fs.Sub(files, "static") 414 if err != nil { 415 log.Fatalf("no static dir found? that's crazy: %v", err) 416 } 417 return http.StripPrefix("/static/", http.FileServer(http.FS(sub))) 418} 419 420func (p *Pages) Error500(w io.Writer) error { 421 return p.execute("errors/500", w, nil) 422} 423 424func (p *Pages) Error404(w io.Writer) error { 425 return p.execute("errors/404", w, nil) 426} 427 428func (p *Pages) Error503(w io.Writer) error { 429 return p.execute("errors/503", w, nil) 430}