b93e14ae88f05c6e01d25ab41d2ecb7456d769b3 / internal/render/render.go · 4897 bytes · raw
// 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 <pre>.
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
}