package render import ( "bytes" "net/url" "path" "path/filepath" "strings" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" ) // rawBase and blobBase are paths from the rendered page to raw/ // and blob/, respectively. func renderMarkdownSafe(src []byte, markdownPath, rawBase, blobBase string) ([]byte, error) { md := goldmark.New( goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions(parser.WithAutoHeadingID()), ) doc := md.Parser().Parse(text.NewReader(src)) if err := ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { switch n := node.(type) { case *ast.Image: n.Destination = []byte(markdownImageURL(string(n.Destination), markdownPath, rawBase)) case *ast.Link: n.Destination = []byte(markdownLinkURL(string(n.Destination), markdownPath, blobBase)) } } return ast.WalkContinue, nil }); err != nil { return nil, err } var buf bytes.Buffer if err := md.Renderer().Render(&buf, src, doc); err != nil { return nil, err } policy := bluemonday.UGCPolicy() return policy.SanitizeBytes(buf.Bytes()), nil } func markdownImageURL(destination, markdownPath, rawBase string) string { return markdownRepositoryURL(destination, markdownPath, rawBase, false) } func markdownLinkURL(destination, markdownPath, blobBase string) string { return markdownRepositoryURL(destination, markdownPath, blobBase, true) } func markdownRepositoryURL(destination, markdownPath, base string, markdownOnly bool) string { u, err := url.Parse(destination) if err != nil || u.IsAbs() || u.Host != "" || strings.HasPrefix(destination, "//") || u.Path == "" { return destination } if markdownOnly && !strings.EqualFold(path.Ext(u.Path), ".md") { return destination } // Resolve within the repository, including repository-root paths. Clamp // parent traversal at its root so destinations cannot escape the revision. repoPath := u.Path if !strings.HasPrefix(repoPath, "/") { repoPath = path.Dir(markdownPath) + "/" + repoPath } repoPath = strings.TrimPrefix(path.Clean("/"+repoPath), "/") u.Path = path.Join(base, repoPath) if markdownOnly { u.Path += ".html" } u.RawPath = "" return u.String() } func isMarkdownPath(path string) bool { name := strings.ToLower(filepath.Base(path)) ext := strings.ToLower(filepath.Ext(path)) return ext == ".md" || name == "readme" } func findReadmePath(tree []TreeEntry) string { for _, e := range tree { if e.Type != "blob" { continue } name := strings.ToLower(e.Path) switch name { case "readme.md", "readme.markdown", "readme": return e.Path } } return "" }