// Package render converts markdown to sanitized HTML (for repo READMEs) and // provides the matching syntax-highlighting stylesheet shared by README code // blocks and blob views. package render import ( "bytes" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/styles" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" ghtml "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/util" ) // chromaStyle is the chroma style whose CSS classes the highlighter emits and // CSS() generates. Class-based (not inline) so the page CSP can stay strict. const chromaStyle = "github" // Renderer turns markdown into sanitized HTML. type Renderer struct { md goldmark.Markdown policy *bluemonday.Policy } // New builds a Renderer with GitHub-flavored markdown + class-based syntax // highlighting, and a sanitizer that permits the highlighter's markup. func New() *Renderer { md := goldmark.New( goldmark.WithExtensions( extension.GFM, highlighting.NewHighlighting( highlighting.WithStyle(chromaStyle), highlighting.WithFormatOptions(chromahtml.WithClasses(true)), ), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), // Divert ```mermaid blocks before the highlighter turns them to code. parser.WithASTTransformers(util.Prioritized(mermaidTransformer{}, 100)), ), goldmark.WithRendererOptions( ghtml.WithUnsafe(), // raw HTML is re-checked by bluemonday below renderer.WithNodeRenderers(util.Prioritized(mermaidHTMLRenderer{}, 100)), ), ) p := bluemonday.UGCPolicy() // Allow the chroma highlighter's class-tagged markup (and the mermaid //
container).
p.AllowAttrs("class").OnElements("span", "code", "pre", "div")
// Heading anchor ids produced by WithAutoHeadingID.
p.AllowAttrs("id").Globally()
// GFM task-list checkboxes.
p.AllowAttrs("type", "checked", "disabled").OnElements("input")
// Tables from GFM.
p.AllowAttrs("align").OnElements("td", "th")
return &Renderer{md: md, policy: p}
}
// ToHTML renders markdown source to sanitized HTML.
func (r *Renderer) ToHTML(src []byte) ([]byte, error) {
var buf bytes.Buffer
if err := r.md.Convert(src, &buf); err != nil {
return nil, err
}
return r.policy.SanitizeBytes(buf.Bytes()), nil
}
// CSS returns the chroma stylesheet for the highlighter's classes. Served as a
// static stylesheet so inline styles are never needed.
func CSS() ([]byte, error) {
style := styles.Get(chromaStyle)
formatter := chromahtml.New(chromahtml.WithClasses(true))
var buf bytes.Buffer
if err := formatter.WriteCSS(&buf, style); err != nil {
return nil, err
}
return buf.Bytes(), nil
}