// Package markup is an umbrella package for all markups and their renderers. package markup import ( "bytes" "net/url" "path" "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/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ) // RendererType defines the type of renderer to use based on context type RendererType int const ( // RendererTypeRepoMarkdown is for repository documentation markdown files RendererTypeRepoMarkdown RendererType = iota // RendererTypeDefault is non-repo markdown, like issues/pulls/comments. RendererTypeDefault ) // RenderContext holds the contextual data for rendering markdown. // It can be initialized empty, and that'll skip any transformations. type RenderContext struct { CamoUrl string CamoSecret string repoinfo.RepoInfo IsDev bool RendererType RendererType } func (rctx *RenderContext) RenderMarkdown(source string) string { md := goldmark.New( goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions(html.WithUnsafe()), ) if rctx != nil { var transformers []util.PrioritizedValue transformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000)) md.Parser().AddOptions( parser.WithASTTransformers(transformers...), ) } var buf bytes.Buffer if err := md.Convert([]byte(source), &buf); err != nil { return source } return buf.String() } func (rctx *RenderContext) Sanitize(html string) string { policy := bluemonday.UGCPolicy() policy.AllowAttrs("align", "style").Globally() policy.AllowStyles( "margin", "padding", "text-align", "font-weight", "text-decoration", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin-left", "margin-right", "margin-top", "margin-bottom", ) return policy.Sanitize(html) } type MarkdownTransformer struct { rctx *RenderContext } func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } switch a.rctx.RendererType { case RendererTypeRepoMarkdown: switch n := n.(type) { case *ast.Link: a.rctx.relativeLinkTransformer(n) case *ast.Image: a.rctx.imageFromKnotTransformer(n) a.rctx.camoImageLinkTransformer(n) } case RendererTypeDefault: switch n := n.(type) { case *ast.Image: a.rctx.imageFromKnotTransformer(n) a.rctx.camoImageLinkTransformer(n) } } return ast.WalkContinue, nil }) } func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) { dst := string(link.Destination) if isAbsoluteUrl(dst) { return } actualPath := rctx.actualPath(dst) newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, actualPath) link.Destination = []byte(newPath) } func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { dst := string(img.Destination) if isAbsoluteUrl(dst) { return } scheme := "https" if rctx.IsDev { scheme = "http" } actualPath := rctx.actualPath(dst) parsedURL := &url.URL{ Scheme: scheme, Host: rctx.Knot, Path: path.Join("/", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name, "raw", url.PathEscape(rctx.RepoInfo.Ref), actualPath), } newPath := parsedURL.String() img.Destination = []byte(newPath) } // actualPath decides when to join the file path with the // current repository directory (essentially only when the link // destination is relative. if it's absolute then we assume the // user knows what they're doing.) func (rctx *RenderContext) actualPath(dst string) string { if path.IsAbs(dst) { return dst } return path.Join(rctx.CurrentDir, dst) } func isAbsoluteUrl(link string) bool { parsed, err := url.Parse(link) if err != nil { return false } return parsed.IsAbs() }