// package highlighting is a extension for the goldmark(http://github.com/yuin/goldmark). // // This extension adds syntax-highlighting to the fenced code blocks using // chroma(https://github.com/alecthomas/chroma). package highlighting import ( "bytes" "io" "strconv" "strings" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" "github.com/alecthomas/chroma" chromahtml "github.com/alecthomas/chroma/formatters/html" "github.com/alecthomas/chroma/lexers" "github.com/alecthomas/chroma/styles" ) // ImmutableAttributes is a read-only interface for ast.Attributes. type ImmutableAttributes interface { // Get returns (value, true) if an attribute associated with given // name exists, otherwise (nil, false) Get(name []byte) (interface{}, bool) // GetString returns (value, true) if an attribute associated with given // name exists, otherwise (nil, false) GetString(name string) (interface{}, bool) // All returns all attributes. All() []ast.Attribute } type immutableAttributes struct { n ast.Node } func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { return a.n.Attribute(name) } func (a *immutableAttributes) GetString(name string) (interface{}, bool) { return a.n.AttributeString(name) } func (a *immutableAttributes) All() []ast.Attribute { if a.n.Attributes() == nil { return []ast.Attribute{} } return a.n.Attributes() } // CodeBlockContext holds contextual information of code highlighting. type CodeBlockContext interface { // Language returns (language, true) if specified, otherwise (nil, false). Language() ([]byte, bool) // Highlighted returns true if this code block can be highlighted, otherwise false. Highlighted() bool // Attributes return attributes of the code block. Attributes() ImmutableAttributes } type codeBlockContext struct { language []byte highlighted bool attributes ImmutableAttributes } func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext { return &codeBlockContext{ language: language, highlighted: highlighted, attributes: attrs, } } func (c *codeBlockContext) Language() ([]byte, bool) { if c.language != nil { return c.language, true } return nil, false } func (c *codeBlockContext) Highlighted() bool { return c.highlighted } func (c *codeBlockContext) Attributes() ImmutableAttributes { return c.attributes } // WrapperRenderer renders wrapper elements like div, pre, etc. type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool) // CodeBlockOptions creates Chroma options per code block. type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option // Config struct holds options for the extension. type Config struct { html.Config // Style is a highlighting style. // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters. Style string // If set, will try to guess language if none provided. // If the guessing fails, we will fall back to a text lexer. // Note that while Chroma's API supports language guessing, the implementation // is not there yet, so you will currently always get the basic text lexer. GuessLanguage bool // FormatOptions is a option related to output formats. // See https://github.com/alecthomas/chroma#the-html-formatter for details. FormatOptions []chromahtml.Option // CSSWriter is an io.Writer that will be used as CSS data output buffer. // If WithClasses() is enabled, you can get CSS data corresponds to the style. CSSWriter io.Writer // CodeBlockOptions allows set Chroma options per code block. CodeBlockOptions CodeBlockOptions // WrapperRenderer allows you to change wrapper elements. WrapperRenderer WrapperRenderer } // NewConfig returns a new Config with defaults. func NewConfig() Config { return Config{ Config: html.NewConfig(), Style: "github", FormatOptions: []chromahtml.Option{}, CSSWriter: nil, WrapperRenderer: nil, CodeBlockOptions: nil, } } // SetOption implements renderer.SetOptioner. func (c *Config) SetOption(name renderer.OptionName, value interface{}) { switch name { case optStyle: c.Style = value.(string) case optFormatOptions: if value != nil { c.FormatOptions = value.([]chromahtml.Option) } case optCSSWriter: c.CSSWriter = value.(io.Writer) case optWrapperRenderer: c.WrapperRenderer = value.(WrapperRenderer) case optCodeBlockOptions: c.CodeBlockOptions = value.(CodeBlockOptions) case optGuessLanguage: c.GuessLanguage = value.(bool) default: c.Config.SetOption(name, value) } } // Option interface is a functional option interface for the extension. type Option interface { renderer.Option // SetHighlightingOption sets given option to the extension. SetHighlightingOption(*Config) } type withHTMLOptions struct { value []html.Option } func (o *withHTMLOptions) SetConfig(c *renderer.Config) { if o.value != nil { for _, v := range o.value { v.(renderer.Option).SetConfig(c) } } } func (o *withHTMLOptions) SetHighlightingOption(c *Config) { if o.value != nil { for _, v := range o.value { v.SetHTMLOption(&c.Config) } } } // WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. func WithHTMLOptions(opts ...html.Option) Option { return &withHTMLOptions{opts} } const optStyle renderer.OptionName = "HighlightingStyle" var highlightLinesAttrName = []byte("hl_lines") var styleAttrName = []byte("hl_style") var nohlAttrName = []byte("nohl") var linenosAttrName = []byte("linenos") var linenosTableAttrValue = []byte("table") var linenosInlineAttrValue = []byte("inline") var linenostartAttrName = []byte("linenostart") type withStyle struct { value string } func (o *withStyle) SetConfig(c *renderer.Config) { c.Options[optStyle] = o.value } func (o *withStyle) SetHighlightingOption(c *Config) { c.Style = o.value } // WithStyle is a functional option that changes highlighting style. func WithStyle(style string) Option { return &withStyle{style} } const optCSSWriter renderer.OptionName = "HighlightingCSSWriter" type withCSSWriter struct { value io.Writer } func (o *withCSSWriter) SetConfig(c *renderer.Config) { c.Options[optCSSWriter] = o.value } func (o *withCSSWriter) SetHighlightingOption(c *Config) { c.CSSWriter = o.value } // WithCSSWriter is a functional option that sets io.Writer for CSS data. func WithCSSWriter(w io.Writer) Option { return &withCSSWriter{w} } const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage" type withGuessLanguage struct { value bool } func (o *withGuessLanguage) SetConfig(c *renderer.Config) { c.Options[optGuessLanguage] = o.value } func (o *withGuessLanguage) SetHighlightingOption(c *Config) { c.GuessLanguage = o.value } // WithGuessLanguage is a functional option that toggles language guessing // if none provided. func WithGuessLanguage(b bool) Option { return &withGuessLanguage{value: b} } const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer" type withWrapperRenderer struct { value WrapperRenderer } func (o *withWrapperRenderer) SetConfig(c *renderer.Config) { c.Options[optWrapperRenderer] = o.value } func (o *withWrapperRenderer) SetHighlightingOption(c *Config) { c.WrapperRenderer = o.value } // WithWrapperRenderer is a functional option that sets WrapperRenderer that // renders wrapper elements like div, pre, etc. func WithWrapperRenderer(w WrapperRenderer) Option { return &withWrapperRenderer{w} } const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions" type withCodeBlockOptions struct { value CodeBlockOptions } func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) { c.Options[optWrapperRenderer] = o.value } func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) { c.CodeBlockOptions = o.value } // WithCodeBlockOptions is a functional option that sets CodeBlockOptions that // allows setting Chroma options per code block. func WithCodeBlockOptions(c CodeBlockOptions) Option { return &withCodeBlockOptions{value: c} } const optFormatOptions renderer.OptionName = "HighlightingFormatOptions" type withFormatOptions struct { value []chromahtml.Option } func (o *withFormatOptions) SetConfig(c *renderer.Config) { if _, ok := c.Options[optFormatOptions]; !ok { c.Options[optFormatOptions] = []chromahtml.Option{} } c.Options[optStyle] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...) } func (o *withFormatOptions) SetHighlightingOption(c *Config) { c.FormatOptions = append(c.FormatOptions, o.value...) } // WithFormatOptions is a functional option that wraps chroma HTML formatter options. func WithFormatOptions(opts ...chromahtml.Option) Option { return &withFormatOptions{opts} } // HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. type HTMLRenderer struct { Config } // NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer { r := &HTMLRenderer{ Config: NewConfig(), } for _, opt := range opts { opt.SetHighlightingOption(&r.Config) } return r } // RegisterFuncs implements NodeRenderer.RegisterFuncs. func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) } func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { if node.Attributes() != nil { return &immutableAttributes{node} } if infostr != nil { attrStartIdx := -1 for idx, char := range infostr { if char == '{' { attrStartIdx = idx break } } if attrStartIdx > 0 { n := ast.NewTextBlock() // dummy node for storing attributes attrStr := infostr[attrStartIdx:] if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { for _, attr := range attrs { n.SetAttribute(attr.Name, attr.Value) } return &immutableAttributes{n} } } } return nil } func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.FencedCodeBlock) if !entering { return ast.WalkContinue, nil } language := n.Language(source) chromaFormatterOptions := make([]chromahtml.Option, len(r.FormatOptions)) copy(chromaFormatterOptions, r.FormatOptions) style := styles.Get(r.Style) nohl := false var info []byte if n.Info != nil { info = n.Info.Segment.Value(source) } attrs := getAttributes(n, info) if attrs != nil { baseLineNumber := 1 if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok { baseLineNumber = int(linenostartAttr.(float64)) chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber)) } if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr { if lines, ok := linesAttr.([]interface{}); ok { var hlRanges [][2]int for _, l := range lines { if ln, ok := l.(float64); ok { hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1}) } if rng, ok := l.([]uint8); ok { slices := strings.Split(string([]byte(rng)), "-") lhs, err := strconv.Atoi(slices[0]) if err != nil { continue } rhs := lhs if len(slices) > 1 { rhs, err = strconv.Atoi(slices[1]) if err != nil { continue } } hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1}) } } chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges)) } } if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr { styleStr := string([]byte(styleAttr.([]uint8))) style = styles.Get(styleStr) } if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr { nohl = true } if linenosAttr, ok := attrs.Get(linenosAttrName); ok { switch v := linenosAttr.(type) { case bool: chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v)) case []uint8: if v != nil { chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true)) } if bytes.Equal(v, linenosTableAttrValue) { chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true)) } else if bytes.Equal(v, linenosInlineAttrValue) { chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false)) } } } } var lexer chroma.Lexer if language != nil { lexer = lexers.Get(string(language)) } if !nohl && (lexer != nil || r.GuessLanguage) { if style == nil { style = styles.Fallback } var buffer bytes.Buffer l := n.Lines().Len() for i := 0; i < l; i++ { line := n.Lines().At(i) buffer.Write(line.Value(source)) } if lexer == nil { lexer = lexers.Analyse(buffer.String()) if lexer == nil { lexer = lexers.Fallback } language = []byte(strings.ToLower(lexer.Config().Name)) } lexer = chroma.Coalesce(lexer) iterator, err := lexer.Tokenise(nil, buffer.String()) if err == nil { c := newCodeBlockContext(language, true, attrs) if r.CodeBlockOptions != nil { chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...) } formatter := chromahtml.New(chromaFormatterOptions...) if r.WrapperRenderer != nil { r.WrapperRenderer(w, c, true) } _ = formatter.Format(w, style, iterator) == nil if r.WrapperRenderer != nil { r.WrapperRenderer(w, c, false) } if r.CSSWriter != nil { _ = formatter.WriteCSS(r.CSSWriter, style) } return ast.WalkContinue, nil } } var c CodeBlockContext if r.WrapperRenderer != nil { c = newCodeBlockContext(language, false, attrs) r.WrapperRenderer(w, c, true) } else { _, _ = w.WriteString("
')
}
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.RawWrite(w, line.Value(source))
}
if r.WrapperRenderer != nil {
r.WrapperRenderer(w, c, false)
} else {
_, _ = w.WriteString("
\n")
}
return ast.WalkContinue, nil
}
type highlighting struct {
options []Option
}
// Highlighting is a goldmark.Extender implementation.
var Highlighting = &highlighting{
options: []Option{},
}
// NewHighlighting returns a new extension with given options.
func NewHighlighting(opts ...Option) goldmark.Extender {
return &highlighting{
options: opts,
}
}
// Extend implements goldmark.Extender.
func (e *highlighting) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewHTMLRenderer(e.options...), 200),
))
}