internal/render/markdown.go
raw ยท 2180 bytes
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 is the path from the rendered page to raw/<revision>.
func renderMarkdownSafe(src []byte, markdownPath, rawBase 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 img, ok := node.(*ast.Image); ok && entering {
img.Destination = []byte(markdownImageURL(string(img.Destination), markdownPath, rawBase))
}
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 {
u, err := url.Parse(destination)
if err != nil || u.IsAbs() || u.Host != "" || strings.HasPrefix(destination, "//") || u.Path == "" {
return destination
}
// Resolve within the repository, including repository-root paths. Clamp
// parent traversal at its root so images cannot escape the raw revision.
imagePath := u.Path
if !strings.HasPrefix(imagePath, "/") {
imagePath = path.Dir(markdownPath) + "/" + imagePath
}
imagePath = strings.TrimPrefix(path.Clean("/"+imagePath), "/")
u.Path = path.Join(rawBase, imagePath)
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 ""
}