// Package render converts repository content for the browser. package render import ( "bytes" "fmt" "github.com/alecthomas/chroma/v2" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" ) // htmlEscaper replaces goldmark's pass-through rendering of raw HTML // (block and inline) with escaped text, so READMEs cannot inject script. type htmlEscaper struct{} func (htmlEscaper) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindHTMLBlock, escapeHTMLBlock) reg.Register(ast.KindRawHTML, escapeRawHTML) } func escapeHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { n := node.(*ast.HTMLBlock) lines := n.Lines() for i := range lines.Len() { segment := lines.At(i) if _, err := w.Write(util.EscapeHTML(segment.Value(source))); err != nil { return ast.WalkStop, err } } if n.HasClosure() { if _, err := w.Write(util.EscapeHTML(n.ClosureLine.Value(source))); err != nil { return ast.WalkStop, err } } } return ast.WalkSkipChildren, nil } func escapeRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { if _, err := w.Write(util.EscapeHTML(node.Text(source))); err != nil { return ast.WalkStop, err } } return ast.WalkSkipChildren, nil } var markdown = goldmark.New( goldmark.WithRendererOptions( renderer.WithNodeRenderers(util.Prioritized(htmlEscaper{}, 50)), ), ) // Markdown renders markdown source to HTML. Raw HTML in the source is // escaped rather than passed through. func Markdown(source []byte) ([]byte, error) { var out bytes.Buffer if err := markdown.Convert(source, &out); err != nil { return nil, fmt.Errorf("render markdown: %w", err) } return out.Bytes(), nil } // highlightStyle inherits chroma's monokai (for complete token coverage) // and overrides the tokens that matter to the simplegit palette. The class // names are emitted by the formatter; their rules come from ChromaCSS. var highlightStyle = func() *chroma.Style { style, err := styles.Get("monokai").Builder(). Add(chroma.Background, "bg:#1e1f3a"). Add(chroma.Text, "#dcdcf5"). Add(chroma.Comment, "italic #8f92c4"). Add(chroma.Keyword, "#a5aef0"). Add(chroma.KeywordConstant, "#e0af68"). Add(chroma.KeywordDeclaration, "#a5aef0"). Add(chroma.KeywordNamespace, "#a5aef0"). Add(chroma.KeywordType, "#e0af68"). Add(chroma.Name, "#dcdcf5"). Add(chroma.NameOther, "#dcdcf5"). Add(chroma.NameConstant, "#e0af68"). Add(chroma.NameException, "#e06c75"). Add(chroma.NameBuiltin, "#e0af68"). Add(chroma.NameClass, "#7bc275"). Add(chroma.NameFunction, "#a5aef0"). Add(chroma.NameDecorator, "#e0af68"). Add(chroma.NameTag, "#a5aef0"). Add(chroma.NameAttribute, "#e0af68"). Add(chroma.String, "#7bc275"). Add(chroma.LiteralStringEscape, "#e0af68"). Add(chroma.Number, "#e0af68"). Add(chroma.Literal, "#7bc275"). Add(chroma.LiteralDate, "#e0af68"). Add(chroma.Operator, "#8f92c4"). Add(chroma.Punctuation, "#8f92c4"). Add(chroma.GenericDeleted, "#e06c75"). Add(chroma.GenericInserted, "#7bc275"). Add(chroma.GenericHeading, "#a5aef0"). Add(chroma.GenericSubheading, "#a5aef0"). Add(chroma.GenericEmph, "italic"). Add(chroma.GenericStrong, "bold"). Add(chroma.Error, "underline #e06c75"). Add(chroma.LineHighlight, "bg:#2a2c52"). Add(chroma.LineNumbers, "#8f92c4"). Add(chroma.LineNumbersTable, "#8f92c4"). Build() if err != nil { panic("render: build highlight style: " + err.Error()) } return style }() // highlighter emits CSS classes rather than inline styles, so the palette // lives in ChromaCSS and can be tuned against design.md. var highlighter = chromahtml.New(chromahtml.WithClasses(true)) // ChromaCSS returns the stylesheet that styles the classes CodeHTML emits. func ChromaCSS() ([]byte, error) { var buf bytes.Buffer if err := highlighter.WriteCSS(&buf, highlightStyle); err != nil { return nil, fmt.Errorf("render chroma css: %w", err) } return buf.Bytes(), nil } // CodeHTML syntax-highlights a file by name. handled=false means no lexer // matched and the caller should fall back to a plain escaped
.
func CodeHTML(filename, content string) (html string, handled bool, err error) {
lexer := lexers.Match(filename)
if lexer == nil {
return "", false, nil
}
it, err := lexer.Tokenise(nil, content)
if err != nil {
return "", true, fmt.Errorf("tokenise %s: %w", filename, err)
}
var buf bytes.Buffer
if err := highlighter.Format(&buf, highlightStyle, it); err != nil {
return "", true, fmt.Errorf("highlight %s: %w", filename, err)
}
return buf.String(), true, nil
}