forked from tangled.org/core
Monorepo for Tangled

appview: pages/markup: serve relative images directly from the knot

authored by anirudh.fi and committed by Tangled e53a3a38 ba0c65a2

Changed files
+108 -160
appview
pages
markup
templates
repo
state
knotserver
+2
.gitignore
··· 7 7 result 8 8 !.gitkeep 9 9 out/ 10 + ./camo/node_modules/* 11 +
+49 -8
appview/pages/markup/markdown.go
··· 3 3 4 4 import ( 5 5 "bytes" 6 + "net/url" 6 7 "path" 7 8 8 9 "github.com/yuin/goldmark" ··· 11 12 "github.com/yuin/goldmark/parser" 12 13 "github.com/yuin/goldmark/text" 13 14 "github.com/yuin/goldmark/util" 15 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 14 16 ) 15 17 16 18 // RendererType defines the type of renderer to use based on context ··· 24 26 // RenderContext holds the contextual data for rendering markdown. 25 27 // It can be initialized empty, and that'll skip any transformations. 26 28 type RenderContext struct { 27 - Ref string 28 - FullRepoName string 29 + repoinfo.RepoInfo 30 + IsDev bool 29 31 RendererType RendererType 30 32 } 31 33 ··· 66 68 67 69 switch a.rctx.RendererType { 68 70 case RendererTypeRepoMarkdown: 69 - if v, ok := n.(*ast.Link); ok { 70 - a.rctx.relativeLinkTransformer(v) 71 + switch n.(type) { 72 + case *ast.Link: 73 + a.rctx.relativeLinkTransformer(n.(*ast.Link)) 74 + case *ast.Image: 75 + a.rctx.imageFromKnotTransformer(n.(*ast.Image)) 71 76 } 72 77 // more types here like RendererTypeIssue/Pull etc. 73 78 } ··· 79 84 func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) { 80 85 dst := string(link.Destination) 81 86 82 - if len(dst) == 0 || dst[0] == '#' || 83 - bytes.Contains(link.Destination, []byte("://")) || 84 - bytes.HasPrefix(link.Destination, []byte("mailto:")) { 87 + if isAbsoluteUrl(dst) { 85 88 return 86 89 } 87 90 88 - newPath := path.Join("/", rctx.FullRepoName, "tree", rctx.Ref, dst) 91 + newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst) 89 92 link.Destination = []byte(newPath) 90 93 } 94 + 95 + func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { 96 + dst := string(img.Destination) 97 + 98 + if isAbsoluteUrl(dst) { 99 + return 100 + } 101 + 102 + // strip leading './' 103 + if len(dst) >= 2 && dst[0:2] == "./" { 104 + dst = dst[2:] 105 + } 106 + 107 + scheme := "https" 108 + if rctx.IsDev { 109 + scheme = "http" 110 + } 111 + parsedURL := &url.URL{ 112 + Scheme: scheme, 113 + Host: rctx.Knot, 114 + Path: path.Join("/", 115 + rctx.RepoInfo.OwnerDid, 116 + rctx.RepoInfo.Name, 117 + "raw", 118 + url.PathEscape(rctx.RepoInfo.Ref), 119 + dst), 120 + } 121 + newPath := parsedURL.String() 122 + img.Destination = []byte(newPath) 123 + } 124 + 125 + func isAbsoluteUrl(link string) bool { 126 + parsed, err := url.Parse(link) 127 + if err != nil { 128 + return false 129 + } 130 + return parsed.IsAbs() 131 + }
+44 -143
appview/pages/pages.go
··· 12 12 "log" 13 13 "net/http" 14 14 "os" 15 - "path" 16 15 "path/filepath" 17 - "slices" 18 16 "strings" 19 17 20 18 "tangled.sh/tangled.sh/core/appview/auth" 21 19 "tangled.sh/tangled.sh/core/appview/db" 22 20 "tangled.sh/tangled.sh/core/appview/pages/markup" 21 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 22 "tangled.sh/tangled.sh/core/appview/pagination" 24 - "tangled.sh/tangled.sh/core/appview/state/userutil" 25 23 "tangled.sh/tangled.sh/core/patchutil" 26 24 "tangled.sh/tangled.sh/core/types" 27 25 ··· 42 40 dev bool 43 41 embedFS embed.FS 44 42 templateDir string // Path to templates on disk for dev mode 43 + rctx *markup.RenderContext 45 44 } 46 45 47 46 func NewPages(dev bool) *Pages { 47 + // initialized with safe defaults, can be overriden per use 48 + rctx := &markup.RenderContext{ 49 + IsDev: dev, 50 + } 51 + 48 52 p := &Pages{ 49 53 t: make(map[string]*template.Template), 50 54 dev: dev, 51 55 embedFS: Files, 56 + rctx: rctx, 52 57 templateDir: "appview/pages", 53 58 } 54 59 ··· 293 298 type ForkRepoParams struct { 294 299 LoggedInUser *auth.User 295 300 Knots []string 296 - RepoInfo RepoInfo 301 + RepoInfo repoinfo.RepoInfo 297 302 } 298 303 299 304 func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { ··· 343 348 } 344 349 345 350 type RepoDescriptionParams struct { 346 - RepoInfo RepoInfo 351 + RepoInfo repoinfo.RepoInfo 347 352 } 348 353 349 354 func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { ··· 354 359 return p.executePlain("repo/fragments/repoDescription", w, params) 355 360 } 356 361 357 - type RepoInfo struct { 358 - Name string 359 - OwnerDid string 360 - OwnerHandle string 361 - Description string 362 - Knot string 363 - RepoAt syntax.ATURI 364 - IsStarred bool 365 - Stats db.RepoStats 366 - Roles RolesInRepo 367 - Source *db.Repo 368 - SourceHandle string 369 - Ref string 370 - DisableFork bool 371 - } 372 - 373 - type RolesInRepo struct { 374 - Roles []string 375 - } 376 - 377 - func (r RolesInRepo) SettingsAllowed() bool { 378 - return slices.Contains(r.Roles, "repo:settings") 379 - } 380 - 381 - func (r RolesInRepo) CollaboratorInviteAllowed() bool { 382 - return slices.Contains(r.Roles, "repo:invite") 383 - } 384 - 385 - func (r RolesInRepo) RepoDeleteAllowed() bool { 386 - return slices.Contains(r.Roles, "repo:delete") 387 - } 388 - 389 - func (r RolesInRepo) IsOwner() bool { 390 - return slices.Contains(r.Roles, "repo:owner") 391 - } 392 - 393 - func (r RolesInRepo) IsCollaborator() bool { 394 - return slices.Contains(r.Roles, "repo:collaborator") 395 - } 396 - 397 - func (r RolesInRepo) IsPushAllowed() bool { 398 - return slices.Contains(r.Roles, "repo:push") 399 - } 400 - 401 - func (r RepoInfo) OwnerWithAt() string { 402 - if r.OwnerHandle != "" { 403 - return fmt.Sprintf("@%s", r.OwnerHandle) 404 - } else { 405 - return r.OwnerDid 406 - } 407 - } 408 - 409 - func (r RepoInfo) FullName() string { 410 - return path.Join(r.OwnerWithAt(), r.Name) 411 - } 412 - 413 - func (r RepoInfo) OwnerWithoutAt() string { 414 - if strings.HasPrefix(r.OwnerWithAt(), "@") { 415 - return strings.TrimPrefix(r.OwnerWithAt(), "@") 416 - } else { 417 - return userutil.FlattenDid(r.OwnerDid) 418 - } 419 - } 420 - 421 - func (r RepoInfo) FullNameWithoutAt() string { 422 - return path.Join(r.OwnerWithoutAt(), r.Name) 423 - } 424 - 425 - func (r RepoInfo) GetTabs() [][]string { 426 - tabs := [][]string{ 427 - {"overview", "/", "square-chart-gantt"}, 428 - {"issues", "/issues", "circle-dot"}, 429 - {"pulls", "/pulls", "git-pull-request"}, 430 - } 431 - 432 - if r.Roles.SettingsAllowed() { 433 - tabs = append(tabs, []string{"settings", "/settings", "cog"}) 434 - } 435 - 436 - return tabs 437 - } 438 - 439 - // each tab on a repo could have some metadata: 440 - // 441 - // issues -> number of open issues etc. 442 - // settings -> a warning icon to setup branch protection? idk 443 - // 444 - // we gather these bits of info here, because go templates 445 - // are difficult to program in 446 - func (r RepoInfo) TabMetadata() map[string]any { 447 - meta := make(map[string]any) 448 - 449 - if r.Stats.PullCount.Open > 0 { 450 - meta["pulls"] = r.Stats.PullCount.Open 451 - } 452 - 453 - if r.Stats.IssueCount.Open > 0 { 454 - meta["issues"] = r.Stats.IssueCount.Open 455 - } 456 - 457 - // more stuff? 458 - 459 - return meta 460 - } 461 - 462 362 type RepoIndexParams struct { 463 363 LoggedInUser *auth.User 464 - RepoInfo RepoInfo 364 + RepoInfo repoinfo.RepoInfo 465 365 Active string 466 366 TagMap map[string][]string 467 367 CommitsTrunc []*object.Commit ··· 479 379 return p.executeRepo("repo/empty", w, params) 480 380 } 481 381 482 - rctx := markup.RenderContext{ 483 - Ref: params.RepoInfo.Ref, 484 - FullRepoName: params.RepoInfo.FullName(), 382 + p.rctx = &markup.RenderContext{ 383 + RepoInfo: params.RepoInfo, 384 + IsDev: p.dev, 385 + RendererType: markup.RendererTypeRepoMarkdown, 485 386 } 486 387 487 388 if params.ReadmeFileName != "" { ··· 489 390 ext := filepath.Ext(params.ReadmeFileName) 490 391 switch ext { 491 392 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 492 - htmlString = rctx.RenderMarkdown(params.Readme) 393 + htmlString = p.rctx.RenderMarkdown(params.Readme) 493 394 params.Raw = false 494 395 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 495 396 default: ··· 504 405 505 406 type RepoLogParams struct { 506 407 LoggedInUser *auth.User 507 - RepoInfo RepoInfo 408 + RepoInfo repoinfo.RepoInfo 508 409 TagMap map[string][]string 509 410 types.RepoLogResponse 510 411 Active string ··· 518 419 519 420 type RepoCommitParams struct { 520 421 LoggedInUser *auth.User 521 - RepoInfo RepoInfo 422 + RepoInfo repoinfo.RepoInfo 522 423 Active string 523 424 EmailToDidOrHandle map[string]string 524 425 ··· 532 433 533 434 type RepoTreeParams struct { 534 435 LoggedInUser *auth.User 535 - RepoInfo RepoInfo 436 + RepoInfo repoinfo.RepoInfo 536 437 Active string 537 438 BreadCrumbs [][]string 538 439 BaseTreeLink string ··· 568 469 569 470 type RepoBranchesParams struct { 570 471 LoggedInUser *auth.User 571 - RepoInfo RepoInfo 472 + RepoInfo repoinfo.RepoInfo 572 473 Active string 573 474 types.RepoBranchesResponse 574 475 } ··· 580 481 581 482 type RepoTagsParams struct { 582 483 LoggedInUser *auth.User 583 - RepoInfo RepoInfo 484 + RepoInfo repoinfo.RepoInfo 584 485 Active string 585 486 types.RepoTagsResponse 586 487 } ··· 592 493 593 494 type RepoBlobParams struct { 594 495 LoggedInUser *auth.User 595 - RepoInfo RepoInfo 496 + RepoInfo repoinfo.RepoInfo 596 497 Active string 597 498 BreadCrumbs [][]string 598 499 ShowRendered bool ··· 607 508 if params.ShowRendered { 608 509 switch markup.GetFormat(params.Path) { 609 510 case markup.FormatMarkdown: 610 - rctx := markup.RenderContext{ 611 - Ref: params.RepoInfo.Ref, 612 - FullRepoName: params.RepoInfo.FullName(), 511 + p.rctx = &markup.RenderContext{ 512 + RepoInfo: params.RepoInfo, 513 + IsDev: p.dev, 613 514 RendererType: markup.RendererTypeRepoMarkdown, 614 515 } 615 - params.RenderedContents = template.HTML(rctx.RenderMarkdown(params.Contents)) 516 + params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents)) 616 517 } 617 518 } 618 519 ··· 657 558 658 559 type RepoSettingsParams struct { 659 560 LoggedInUser *auth.User 660 - RepoInfo RepoInfo 561 + RepoInfo repoinfo.RepoInfo 661 562 Collaborators []Collaborator 662 563 Active string 663 564 Branches []string ··· 673 574 674 575 type RepoIssuesParams struct { 675 576 LoggedInUser *auth.User 676 - RepoInfo RepoInfo 577 + RepoInfo repoinfo.RepoInfo 677 578 Active string 678 579 Issues []db.Issue 679 580 DidHandleMap map[string]string ··· 688 589 689 590 type RepoSingleIssueParams struct { 690 591 LoggedInUser *auth.User 691 - RepoInfo RepoInfo 592 + RepoInfo repoinfo.RepoInfo 692 593 Active string 693 594 Issue db.Issue 694 595 Comments []db.Comment ··· 710 611 711 612 type RepoNewIssueParams struct { 712 613 LoggedInUser *auth.User 713 - RepoInfo RepoInfo 614 + RepoInfo repoinfo.RepoInfo 714 615 Active string 715 616 } 716 617 ··· 721 622 722 623 type EditIssueCommentParams struct { 723 624 LoggedInUser *auth.User 724 - RepoInfo RepoInfo 625 + RepoInfo repoinfo.RepoInfo 725 626 Issue *db.Issue 726 627 Comment *db.Comment 727 628 } ··· 733 634 type SingleIssueCommentParams struct { 734 635 LoggedInUser *auth.User 735 636 DidHandleMap map[string]string 736 - RepoInfo RepoInfo 637 + RepoInfo repoinfo.RepoInfo 737 638 Issue *db.Issue 738 639 Comment *db.Comment 739 640 } ··· 744 645 745 646 type RepoNewPullParams struct { 746 647 LoggedInUser *auth.User 747 - RepoInfo RepoInfo 648 + RepoInfo repoinfo.RepoInfo 748 649 Branches []types.Branch 749 650 Active string 750 651 } ··· 756 657 757 658 type RepoPullsParams struct { 758 659 LoggedInUser *auth.User 759 - RepoInfo RepoInfo 660 + RepoInfo repoinfo.RepoInfo 760 661 Pulls []*db.Pull 761 662 Active string 762 663 DidHandleMap map[string]string ··· 788 689 789 690 type RepoSinglePullParams struct { 790 691 LoggedInUser *auth.User 791 - RepoInfo RepoInfo 692 + RepoInfo repoinfo.RepoInfo 792 693 Active string 793 694 DidHandleMap map[string]string 794 695 Pull *db.Pull ··· 804 705 type RepoPullPatchParams struct { 805 706 LoggedInUser *auth.User 806 707 DidHandleMap map[string]string 807 - RepoInfo RepoInfo 708 + RepoInfo repoinfo.RepoInfo 808 709 Pull *db.Pull 809 710 Diff *types.NiceDiff 810 711 Round int ··· 819 720 type RepoPullInterdiffParams struct { 820 721 LoggedInUser *auth.User 821 722 DidHandleMap map[string]string 822 - RepoInfo RepoInfo 723 + RepoInfo repoinfo.RepoInfo 823 724 Pull *db.Pull 824 725 Round int 825 726 Interdiff *patchutil.InterdiffResult ··· 831 732 } 832 733 833 734 type PullPatchUploadParams struct { 834 - RepoInfo RepoInfo 735 + RepoInfo repoinfo.RepoInfo 835 736 } 836 737 837 738 func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { ··· 839 740 } 840 741 841 742 type PullCompareBranchesParams struct { 842 - RepoInfo RepoInfo 743 + RepoInfo repoinfo.RepoInfo 843 744 Branches []types.Branch 844 745 } 845 746 ··· 848 749 } 849 750 850 751 type PullCompareForkParams struct { 851 - RepoInfo RepoInfo 752 + RepoInfo repoinfo.RepoInfo 852 753 Forks []db.Repo 853 754 } 854 755 ··· 857 758 } 858 759 859 760 type PullCompareForkBranchesParams struct { 860 - RepoInfo RepoInfo 761 + RepoInfo repoinfo.RepoInfo 861 762 SourceBranches []types.Branch 862 763 TargetBranches []types.Branch 863 764 } ··· 868 769 869 770 type PullResubmitParams struct { 870 771 LoggedInUser *auth.User 871 - RepoInfo RepoInfo 772 + RepoInfo repoinfo.RepoInfo 872 773 Pull *db.Pull 873 774 SubmissionId int 874 775 } ··· 879 780 880 781 type PullActionsParams struct { 881 782 LoggedInUser *auth.User 882 - RepoInfo RepoInfo 783 + RepoInfo repoinfo.RepoInfo 883 784 Pull *db.Pull 884 785 RoundNumber int 885 786 MergeCheck types.MergeCheckResponse ··· 892 793 893 794 type PullNewCommentParams struct { 894 795 LoggedInUser *auth.User 895 - RepoInfo RepoInfo 796 + RepoInfo repoinfo.RepoInfo 896 797 Pull *db.Pull 897 798 RoundNumber int 898 799 }
+1 -1
appview/pages/templates/repo/blob.html
··· 42 42 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 43 43 <span>{{ byteFmt .SizeHint }}</span> 44 44 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 45 - <a href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/raw/{{ .Path }}">view raw</a> 45 + <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 46 46 {{ if .RenderToggle }} 47 47 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 48 48 <a
+3 -2
appview/state/repo.go
··· 28 28 "tangled.sh/tangled.sh/core/appview/db" 29 29 "tangled.sh/tangled.sh/core/appview/pages" 30 30 "tangled.sh/tangled.sh/core/appview/pages/markup" 31 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 31 32 "tangled.sh/tangled.sh/core/appview/pagination" 32 33 "tangled.sh/tangled.sh/core/types" 33 34 ··· 996 997 return collaborators, nil 997 998 } 998 999 999 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 1000 + func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { 1000 1001 isStarred := false 1001 1002 if u != nil { 1002 1003 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) ··· 1070 1071 knot = "tangled.sh" 1071 1072 } 1072 1073 1073 - repoInfo := pages.RepoInfo{ 1074 + repoInfo := repoinfo.RepoInfo{ 1074 1075 OwnerDid: f.OwnerDid(), 1075 1076 OwnerHandle: f.OwnerHandle(), 1076 1077 Name: f.RepoName,
+4 -4
appview/state/repo_util.go
··· 14 14 "github.com/go-git/go-git/v5/plumbing/object" 15 15 "tangled.sh/tangled.sh/core/appview/auth" 16 16 "tangled.sh/tangled.sh/core/appview/db" 17 - "tangled.sh/tangled.sh/core/appview/pages" 17 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 18 18 ) 19 19 20 20 func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { ··· 73 73 }, nil 74 74 } 75 75 76 - func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 76 + func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { 77 77 if u != nil { 78 78 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 79 - return pages.RolesInRepo{r} 79 + return repoinfo.RolesInRepo{r} 80 80 } else { 81 - return pages.RolesInRepo{} 81 + return repoinfo.RolesInRepo{} 82 82 } 83 83 } 84 84
+1 -1
appview/state/router.go
··· 65 65 r.Get("/branches", s.RepoBranches) 66 66 r.Get("/tags", s.RepoTags) 67 67 r.Get("/blob/{ref}/*", s.RepoBlob) 68 - r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 68 + r.Get("/raw/{ref}/*", s.RepoBlobRaw) 69 69 70 70 r.Route("/issues", func(r chi.Router) { 71 71 r.With(middleware.Paginate).Get("/", s.RepoIssues)
+4 -1
knotserver/handler.go
··· 100 100 101 101 r.Route("/blob/{ref}", func(r chi.Router) { 102 102 r.Get("/*", h.Blob) 103 - r.Get("/raw/*", h.BlobRaw) 103 + }) 104 + 105 + r.Route("/raw/{ref}", func(r chi.Router) { 106 + r.Get("/*", h.BlobRaw) 104 107 }) 105 108 106 109 r.Get("/log/{ref}", h.Log)