penguin/flatgit

flatgit showcase

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 ""
}