forked from tangled.org/core
Monorepo for Tangled

appview: pages/markup: resolve link destinations against the current dir

Fixes a bug reported on Discord with relative links inside a
repository's subdir would resolve incorrectly since we were naively
"absoluting" the link destination.

Now, we resolve it against the current (parent) directory. For example,
if lol/x.md has a link

[foo](./some.png) => /lol/some.png (instead of just /some.png)

authored by anirudh.fi and committed by Tangled cd60c898 3f274571

Changed files
+87 -17
appview
pages
markup
repoinfo
state
+50 -15
appview/pages/markup/markdown.go
··· 6 6 "net/url" 7 7 "path" 8 8 9 + "github.com/microcosm-cc/bluemonday" 9 10 "github.com/yuin/goldmark" 10 11 "github.com/yuin/goldmark/ast" 11 12 "github.com/yuin/goldmark/extension" ··· 13 14 "github.com/yuin/goldmark/renderer/html" 14 15 "github.com/yuin/goldmark/text" 15 16 "github.com/yuin/goldmark/util" 17 + 16 18 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 17 19 ) 18 20 ··· 62 64 return buf.String() 63 65 } 64 66 67 + func (rctx *RenderContext) Sanitize(html string) string { 68 + policy := bluemonday.UGCPolicy() 69 + policy.AllowAttrs("align", "style").Globally() 70 + policy.AllowStyles( 71 + "margin", 72 + "padding", 73 + "text-align", 74 + "font-weight", 75 + "text-decoration", 76 + "padding-left", 77 + "padding-right", 78 + "padding-top", 79 + "padding-bottom", 80 + "margin-left", 81 + "margin-right", 82 + "margin-top", 83 + "margin-bottom", 84 + ) 85 + return policy.Sanitize(html) 86 + } 87 + 65 88 type MarkdownTransformer struct { 66 89 rctx *RenderContext 67 90 } ··· 74 97 75 98 switch a.rctx.RendererType { 76 99 case RendererTypeRepoMarkdown: 77 - switch n.(type) { 100 + switch n := n.(type) { 78 101 case *ast.Link: 79 - a.rctx.relativeLinkTransformer(n.(*ast.Link)) 102 + a.rctx.relativeLinkTransformer(n) 80 103 case *ast.Image: 81 - a.rctx.imageFromKnotTransformer(n.(*ast.Image)) 82 - a.rctx.camoImageLinkTransformer(n.(*ast.Image)) 104 + a.rctx.imageFromKnotTransformer(n) 105 + a.rctx.camoImageLinkTransformer(n) 83 106 } 84 - 85 107 case RendererTypeDefault: 86 - switch n.(type) { 108 + switch n := n.(type) { 87 109 case *ast.Image: 88 - a.rctx.imageFromKnotTransformer(n.(*ast.Image)) 89 - a.rctx.camoImageLinkTransformer(n.(*ast.Image)) 110 + a.rctx.imageFromKnotTransformer(n) 111 + a.rctx.camoImageLinkTransformer(n) 90 112 } 91 113 } 92 114 ··· 95 117 } 96 118 97 119 func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) { 120 + 98 121 dst := string(link.Destination) 99 122 100 123 if isAbsoluteUrl(dst) { 101 124 return 102 125 } 103 126 104 - newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst) 127 + actualPath := rctx.actualPath(dst) 128 + 129 + newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, actualPath) 105 130 link.Destination = []byte(newPath) 106 131 } 107 132 ··· 112 137 return 113 138 } 114 139 115 - // strip leading './' 116 - if len(dst) >= 2 && dst[0:2] == "./" { 117 - dst = dst[2:] 118 - } 119 - 120 140 scheme := "https" 121 141 if rctx.IsDev { 122 142 scheme = "http" 123 143 } 144 + 145 + actualPath := rctx.actualPath(dst) 146 + 124 147 parsedURL := &url.URL{ 125 148 Scheme: scheme, 126 149 Host: rctx.Knot, ··· 129 152 rctx.RepoInfo.Name, 130 153 "raw", 131 154 url.PathEscape(rctx.RepoInfo.Ref), 132 - dst), 155 + actualPath), 133 156 } 134 157 newPath := parsedURL.String() 135 158 img.Destination = []byte(newPath) 159 + } 160 + 161 + // actualPath decides when to join the file path with the 162 + // current repository directory (essentially only when the link 163 + // destination is relative. if it's absolute then we assume the 164 + // user knows what they're doing.) 165 + func (rctx *RenderContext) actualPath(dst string) string { 166 + if path.IsAbs(dst) { 167 + return dst 168 + } 169 + 170 + return path.Join(rctx.CurrentDir, dst) 136 171 } 137 172 138 173 func isAbsoluteUrl(link string) bool {
+3 -2
appview/pages/pages.go
··· 432 432 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 433 433 htmlString = p.rctx.RenderMarkdown(params.Readme) 434 434 params.Raw = false 435 - params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 435 + params.HTMLReadme = template.HTML(p.rctx.Sanitize(htmlString)) 436 436 default: 437 437 htmlString = string(params.Readme) 438 438 params.Raw = true ··· 562 562 case markup.FormatMarkdown: 563 563 p.rctx.RepoInfo = params.RepoInfo 564 564 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 565 - params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents))) 565 + htmlString := p.rctx.RenderMarkdown(params.Contents) 566 + params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 566 567 } 567 568 } 568 569
+1
appview/pages/repoinfo/repoinfo.go
··· 63 63 SourceHandle string 64 64 Ref string 65 65 DisableFork bool 66 + CurrentDir string 66 67 } 67 68 68 69 // each tab on a repo could have some metadata:
+2
appview/state/repo.go
··· 946 946 Description string 947 947 CreatedAt string 948 948 Ref string 949 + CurrentDir string 949 950 } 950 951 951 952 func (f *FullyResolvedRepo) OwnerDid() string { ··· 1104 1105 PullCount: pullCount, 1105 1106 }, 1106 1107 DisableFork: disableFork, 1108 + CurrentDir: f.CurrentDir, 1107 1109 } 1108 1110 1109 1111 if sourceRepo != nil {
+31
appview/state/repo_util.go
··· 7 7 "log" 8 8 "math/big" 9 9 "net/http" 10 + "net/url" 11 + "path" 12 + "strings" 10 13 11 14 "github.com/bluesky-social/indigo/atproto/identity" 12 15 "github.com/bluesky-social/indigo/atproto/syntax" ··· 59 62 ref = defaultBranch.Branch 60 63 } 61 64 65 + currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 66 + 62 67 // pass through values from the middleware 63 68 description, ok := r.Context().Value("repoDescription").(string) 64 69 addedAt, ok := r.Context().Value("repoAddedAt").(string) ··· 71 76 Description: description, 72 77 CreatedAt: addedAt, 73 78 Ref: ref, 79 + CurrentDir: currentDir, 74 80 }, nil 75 81 } 76 82 ··· 81 87 } else { 82 88 return repoinfo.RolesInRepo{} 83 89 } 90 + } 91 + 92 + // extractPathAfterRef gets the actual repository path 93 + // after the ref. for example: 94 + // 95 + // /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 96 + func extractPathAfterRef(fullPath, ref string) string { 97 + fullPath = strings.TrimPrefix(fullPath, "/") 98 + 99 + ref = url.PathEscape(ref) 100 + 101 + prefixes := []string{ 102 + fmt.Sprintf("blob/%s/", ref), 103 + fmt.Sprintf("tree/%s/", ref), 104 + fmt.Sprintf("raw/%s/", ref), 105 + } 106 + 107 + for _, prefix := range prefixes { 108 + idx := strings.Index(fullPath, prefix) 109 + if idx != -1 { 110 + return fullPath[idx+len(prefix):] 111 + } 112 + } 113 + 114 + return "" 84 115 } 85 116 86 117 func uniqueEmails(commits []*object.Commit) []string {