internal/render/markdown.go
raw ยท 2808 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 and blobBase are paths from the rendered page to raw/<revision>
// and blob/<revision>, 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 ""
}