|
- // 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
-
- // Pass in a custom Chroma style. If this is not nil, the Style string will be ignored
- CustomStyle *chroma.Style
-
- // 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 optCustomStyle:
- c.CustomStyle = value.(*chroma.Style)
- 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"
- const optCustomStyle renderer.OptionName = "HighlightingCustomStyle"
-
- 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}
- }
-
- type withCustomStyle struct {
- value *chroma.Style
- }
-
- func (o *withCustomStyle) SetConfig(c *renderer.Config) {
- c.Options[optCustomStyle] = o.value
- }
-
- func (o *withCustomStyle) SetHighlightingOption(c *Config) {
- c.CustomStyle = o.value
- }
-
- // WithStyle is a functional option that changes highlighting style.
- func WithCustomStyle(style *chroma.Style) Option {
- return &withCustomStyle{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[optFormatOptions] = 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 := r.CustomStyle
- if style == nil {
- 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 {
- if linenostart, ok := linenostartAttr.(float64); ok {
- baseLineNumber = int(linenostart)
- 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 {
- if st, ok := styleAttr.([]uint8); ok {
- styleStr := string([]byte(st))
- 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("<pre><code")
- language := n.Language(source)
- if language != nil {
- _, _ = w.WriteString(" class=\"language-")
- r.Writer.Write(w, language)
- _, _ = w.WriteString("\"")
- }
- _ = w.WriteByte('>')
- }
- 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("</code></pre>\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),
- ))
- }
|