github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/markup/highlight/highlight.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package highlight
    15  
    16  import (
    17  	"fmt"
    18  	gohtml "html"
    19  	"html/template"
    20  	"io"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/alecthomas/chroma"
    25  	"github.com/alecthomas/chroma/formatters/html"
    26  	"github.com/alecthomas/chroma/lexers"
    27  	"github.com/alecthomas/chroma/styles"
    28  	"github.com/gohugoio/hugo/common/hugio"
    29  	"github.com/gohugoio/hugo/identity"
    30  	"github.com/gohugoio/hugo/markup/converter/hooks"
    31  	"github.com/gohugoio/hugo/markup/internal/attributes"
    32  )
    33  
    34  // Markdown attributes used by the Chroma hightlighter.
    35  var chromaHightlightProcessingAttributes = map[string]bool{
    36  	"anchorLineNos":      true,
    37  	"guessSyntax":        true,
    38  	"hl_Lines":           true,
    39  	"lineAnchors":        true,
    40  	"lineNos":            true,
    41  	"lineNoStart":        true,
    42  	"lineNumbersInTable": true,
    43  	"noClasses":          true,
    44  	"style":              true,
    45  	"tabWidth":           true,
    46  }
    47  
    48  func init() {
    49  	for k, v := range chromaHightlightProcessingAttributes {
    50  		chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
    51  	}
    52  }
    53  
    54  func New(cfg Config) Highlighter {
    55  	return chromaHighlighter{
    56  		cfg: cfg,
    57  	}
    58  }
    59  
    60  type Highlighter interface {
    61  	Highlight(code, lang string, opts interface{}) (string, error)
    62  	HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error)
    63  	hooks.CodeBlockRenderer
    64  }
    65  
    66  type chromaHighlighter struct {
    67  	cfg Config
    68  }
    69  
    70  func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) {
    71  	cfg := h.cfg
    72  	if err := applyOptions(opts, &cfg); err != nil {
    73  		return "", err
    74  	}
    75  	var b strings.Builder
    76  
    77  	if err := highlight(&b, code, lang, nil, cfg); err != nil {
    78  		return "", err
    79  	}
    80  
    81  	return b.String(), nil
    82  }
    83  
    84  func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) {
    85  	cfg := h.cfg
    86  
    87  	var b strings.Builder
    88  
    89  	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
    90  	options := ctx.Options()
    91  
    92  	if err := applyOptionsFromMap(options, &cfg); err != nil {
    93  		return HightlightResult{}, err
    94  	}
    95  
    96  	// Apply these last so the user can override them.
    97  	if err := applyOptions(opts, &cfg); err != nil {
    98  		return HightlightResult{}, err
    99  	}
   100  
   101  	err := highlight(&b, ctx.Code(), ctx.Lang(), attributes, cfg)
   102  	if err != nil {
   103  		return HightlightResult{}, err
   104  	}
   105  
   106  	return HightlightResult{
   107  		Body: template.HTML(b.String()),
   108  	}, nil
   109  }
   110  
   111  func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
   112  	cfg := h.cfg
   113  	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
   114  
   115  	if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
   116  		return err
   117  	}
   118  
   119  	return highlight(w, ctx.Code(), ctx.Lang(), attributes, cfg)
   120  }
   121  
   122  var id = identity.NewPathIdentity("chroma", "highlight")
   123  
   124  func (h chromaHighlighter) GetIdentity() identity.Identity {
   125  	return id
   126  }
   127  
   128  type HightlightResult struct {
   129  	Body template.HTML
   130  }
   131  
   132  func (h HightlightResult) Highlighted() template.HTML {
   133  	return h.Body
   134  }
   135  
   136  func (h chromaHighlighter) toHighlightOptionsAttributes(ctx hooks.CodeblockContext) (map[string]interface{}, map[string]interface{}) {
   137  	attributes := ctx.Attributes()
   138  	if attributes == nil || len(attributes) == 0 {
   139  		return nil, nil
   140  	}
   141  
   142  	options := make(map[string]interface{})
   143  	attrs := make(map[string]interface{})
   144  
   145  	for k, v := range attributes {
   146  		klow := strings.ToLower(k)
   147  		if chromaHightlightProcessingAttributes[klow] {
   148  			options[klow] = v
   149  		} else {
   150  			attrs[k] = v
   151  		}
   152  	}
   153  	const lineanchorsKey = "lineanchors"
   154  	if _, found := options[lineanchorsKey]; !found {
   155  		// Set it to the ordinal.
   156  		options[lineanchorsKey] = strconv.Itoa(ctx.Ordinal())
   157  	}
   158  	return options, attrs
   159  }
   160  
   161  func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error {
   162  	var lexer chroma.Lexer
   163  	if lang != "" {
   164  		lexer = lexers.Get(lang)
   165  	}
   166  
   167  	if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
   168  		lexer = lexers.Analyse(code)
   169  		if lexer == nil {
   170  			lexer = lexers.Fallback
   171  		}
   172  		lang = strings.ToLower(lexer.Config().Name)
   173  	}
   174  
   175  	if lexer == nil {
   176  		wrapper := getPreWrapper(lang)
   177  		fmt.Fprint(w, wrapper.Start(true, ""))
   178  		fmt.Fprint(w, gohtml.EscapeString(code))
   179  		fmt.Fprint(w, wrapper.End(true))
   180  		return nil
   181  	}
   182  
   183  	style := styles.Get(cfg.Style)
   184  	if style == nil {
   185  		style = styles.Fallback
   186  	}
   187  	lexer = chroma.Coalesce(lexer)
   188  
   189  	iterator, err := lexer.Tokenise(nil, code)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	options := cfg.ToHTMLOptions()
   195  	options = append(options, getHtmlPreWrapper(lang))
   196  
   197  	formatter := html.New(options...)
   198  
   199  	writeDivStart(w, attributes)
   200  	if err := formatter.Format(w, style, iterator); err != nil {
   201  		return err
   202  	}
   203  	writeDivEnd(w)
   204  
   205  	return nil
   206  }
   207  
   208  func getPreWrapper(language string) preWrapper {
   209  	return preWrapper{language: language}
   210  }
   211  
   212  func getHtmlPreWrapper(language string) html.Option {
   213  	return html.WithPreWrapper(getPreWrapper(language))
   214  }
   215  
   216  type preWrapper struct {
   217  	language string
   218  }
   219  
   220  func (p preWrapper) Start(code bool, styleAttr string) string {
   221  	var language string
   222  	if code {
   223  		language = p.language
   224  	}
   225  	w := &strings.Builder{}
   226  	WritePreStart(w, language, styleAttr)
   227  	return w.String()
   228  }
   229  
   230  func WritePreStart(w io.Writer, language, styleAttr string) {
   231  	fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr)
   232  	fmt.Fprint(w, "<code")
   233  	if language != "" {
   234  		fmt.Fprint(w, ` class="language-`+language+`"`)
   235  		fmt.Fprint(w, ` data-lang="`+language+`"`)
   236  	}
   237  	fmt.Fprint(w, ">")
   238  }
   239  
   240  const preEnd = "</code></pre>"
   241  
   242  func (p preWrapper) End(code bool) string {
   243  	return preEnd
   244  }
   245  
   246  func WritePreEnd(w io.Writer) {
   247  	fmt.Fprint(w, preEnd)
   248  }
   249  
   250  func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
   251  	w.WriteString(`<div class="highlight`)
   252  	if attrs != nil {
   253  		for _, attr := range attrs {
   254  			if attr.Name == "class" {
   255  				w.WriteString(" " + attr.ValueString())
   256  				break
   257  			}
   258  		}
   259  		_, _ = w.WriteString("\"")
   260  		attributes.RenderAttributes(w, true, attrs...)
   261  	} else {
   262  		_, _ = w.WriteString("\"")
   263  	}
   264  
   265  	w.WriteString(">")
   266  }
   267  
   268  func writeDivEnd(w hugio.FlexiWriter) {
   269  	w.WriteString("</div>")
   270  }