forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

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)

Changed files
+87 -17
appview
pages
markup
repoinfo
state
+50 -15
appview/pages/markup/markdown.go
···
"net/url"
"path"
+
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
···
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
)
···
return buf.String()
}
+
func (rctx *RenderContext) Sanitize(html string) string {
+
policy := bluemonday.UGCPolicy()
+
policy.AllowAttrs("align", "style").Globally()
+
policy.AllowStyles(
+
"margin",
+
"padding",
+
"text-align",
+
"font-weight",
+
"text-decoration",
+
"padding-left",
+
"padding-right",
+
"padding-top",
+
"padding-bottom",
+
"margin-left",
+
"margin-right",
+
"margin-top",
+
"margin-bottom",
+
)
+
return policy.Sanitize(html)
+
}
+
type MarkdownTransformer struct {
rctx *RenderContext
}
···
switch a.rctx.RendererType {
case RendererTypeRepoMarkdown:
-
switch n.(type) {
+
switch n := n.(type) {
case *ast.Link:
-
a.rctx.relativeLinkTransformer(n.(*ast.Link))
+
a.rctx.relativeLinkTransformer(n)
case *ast.Image:
-
a.rctx.imageFromKnotTransformer(n.(*ast.Image))
-
a.rctx.camoImageLinkTransformer(n.(*ast.Image))
+
a.rctx.imageFromKnotTransformer(n)
+
a.rctx.camoImageLinkTransformer(n)
}
-
case RendererTypeDefault:
-
switch n.(type) {
+
switch n := n.(type) {
case *ast.Image:
-
a.rctx.imageFromKnotTransformer(n.(*ast.Image))
-
a.rctx.camoImageLinkTransformer(n.(*ast.Image))
+
a.rctx.imageFromKnotTransformer(n)
+
a.rctx.camoImageLinkTransformer(n)
}
}
···
}
func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {
+
dst := string(link.Destination)
if isAbsoluteUrl(dst) {
return
}
-
newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst)
+
actualPath := rctx.actualPath(dst)
+
+
newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, actualPath)
link.Destination = []byte(newPath)
}
···
return
}
-
// strip leading './'
-
if len(dst) >= 2 && dst[0:2] == "./" {
-
dst = dst[2:]
-
}
-
scheme := "https"
if rctx.IsDev {
scheme = "http"
}
+
+
actualPath := rctx.actualPath(dst)
+
parsedURL := &url.URL{
Scheme: scheme,
Host: rctx.Knot,
···
rctx.RepoInfo.Name,
"raw",
url.PathEscape(rctx.RepoInfo.Ref),
-
dst),
+
actualPath),
}
newPath := parsedURL.String()
img.Destination = []byte(newPath)
+
}
+
+
// actualPath decides when to join the file path with the
+
// current repository directory (essentially only when the link
+
// destination is relative. if it's absolute then we assume the
+
// user knows what they're doing.)
+
func (rctx *RenderContext) actualPath(dst string) string {
+
if path.IsAbs(dst) {
+
return dst
+
}
+
+
return path.Join(rctx.CurrentDir, dst)
}
func isAbsoluteUrl(link string) bool {
+3 -2
appview/pages/pages.go
···
case ".md", ".markdown", ".mdown", ".mkdn", ".mkd":
htmlString = p.rctx.RenderMarkdown(params.Readme)
params.Raw = false
-
params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString))
+
params.HTMLReadme = template.HTML(p.rctx.Sanitize(htmlString))
default:
htmlString = string(params.Readme)
params.Raw = true
···
case markup.FormatMarkdown:
p.rctx.RepoInfo = params.RepoInfo
p.rctx.RendererType = markup.RendererTypeRepoMarkdown
-
params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents)))
+
htmlString := p.rctx.RenderMarkdown(params.Contents)
+
params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString))
}
}
+1
appview/pages/repoinfo/repoinfo.go
···
SourceHandle string
Ref string
DisableFork bool
+
CurrentDir string
}
// each tab on a repo could have some metadata:
+2
appview/state/repo.go
···
Description string
CreatedAt string
Ref string
+
CurrentDir string
}
func (f *FullyResolvedRepo) OwnerDid() string {
···
PullCount: pullCount,
},
DisableFork: disableFork,
+
CurrentDir: f.CurrentDir,
if sourceRepo != nil {
+31
appview/state/repo_util.go
···
"log"
"math/big"
"net/http"
+
"net/url"
+
"path"
+
"strings"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
···
ref = defaultBranch.Branch
}
+
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref))
+
// pass through values from the middleware
description, ok := r.Context().Value("repoDescription").(string)
addedAt, ok := r.Context().Value("repoAddedAt").(string)
···
Description: description,
CreatedAt: addedAt,
Ref: ref,
+
CurrentDir: currentDir,
}, nil
}
···
} else {
return repoinfo.RolesInRepo{}
}
+
}
+
+
// extractPathAfterRef gets the actual repository path
+
// after the ref. for example:
+
//
+
// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
+
func extractPathAfterRef(fullPath, ref string) string {
+
fullPath = strings.TrimPrefix(fullPath, "/")
+
+
ref = url.PathEscape(ref)
+
+
prefixes := []string{
+
fmt.Sprintf("blob/%s/", ref),
+
fmt.Sprintf("tree/%s/", ref),
+
fmt.Sprintf("raw/%s/", ref),
+
}
+
+
for _, prefix := range prefixes {
+
idx := strings.Index(fullPath, prefix)
+
if idx != -1 {
+
return fullPath[idx+len(prefix):]
+
}
+
}
+
+
return ""
}
func uniqueEmails(commits []*object.Commit) []string {