diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go index e41c003c..5d3a3a7c 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -6,6 +6,7 @@ import ( "net/url" "path" + "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" @@ -13,6 +14,7 @@ import ( "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ) @@ -62,6 +64,27 @@ func (rctx *RenderContext) RenderMarkdown(source string) string { 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 } @@ -74,19 +97,18 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, 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) } } @@ -95,13 +117,16 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, } 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) } @@ -112,15 +137,13 @@ func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { 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, @@ -129,12 +152,24 @@ func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { 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 { parsed, err := url.Parse(link) if err != nil { diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 8a74a3c3..eb3c079c 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -432,7 +432,7 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 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 @@ -562,7 +562,8 @@ func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 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)) } } diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go index c9f54eca..8b1ca8c8 100644 --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -63,6 +63,7 @@ type RepoInfo struct { SourceHandle string Ref string DisableFork bool + CurrentDir string } // each tab on a repo could have some metadata: diff --git a/appview/state/repo.go b/appview/state/repo.go index 2a1d9407..1e7b3fe9 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -946,6 +946,7 @@ type FullyResolvedRepo struct { Description string CreatedAt string Ref string + CurrentDir string } func (f *FullyResolvedRepo) OwnerDid() string { @@ -1104,6 +1105,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo PullCount: pullCount, }, DisableFork: disableFork, + CurrentDir: f.CurrentDir, } if sourceRepo != nil { diff --git a/appview/state/repo_util.go b/appview/state/repo_util.go index 7b4f6a39..aaf643ea 100644 --- a/appview/state/repo_util.go +++ b/appview/state/repo_util.go @@ -7,6 +7,9 @@ import ( "log" "math/big" "net/http" + "net/url" + "path" + "strings" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" @@ -59,6 +62,8 @@ func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 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) @@ -71,6 +76,7 @@ func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { Description: description, CreatedAt: addedAt, Ref: ref, + CurrentDir: currentDir, }, nil } @@ -83,6 +89,31 @@ func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesIn } } +// 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 { emails := make(map[string]struct{}) for _, commit := range commits {