penguin/flatgit

flatgit showcase

internal/render/markdown.go

raw ยท 2899 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))
				n.SetAttributeString("data-md", "img")
			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()
	policy.AllowAttrs("data-md").OnElements("img")
	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 ""
}