github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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  	"context"
    18  	"fmt"
    19  	gohtml "html"
    20  	"html/template"
    21  	"io"
    22  	"strings"
    23  
    24  	"github.com/alecthomas/chroma/v2"
    25  	"github.com/alecthomas/chroma/v2/formatters/html"
    26  	"github.com/alecthomas/chroma/v2/lexers"
    27  	"github.com/alecthomas/chroma/v2/styles"
    28  	"github.com/gohugoio/hugo/common/hugio"
    29  	"github.com/gohugoio/hugo/common/text"
    30  	"github.com/gohugoio/hugo/identity"
    31  	"github.com/gohugoio/hugo/markup/converter/hooks"
    32  	"github.com/gohugoio/hugo/markup/highlight/chromalexers"
    33  	"github.com/gohugoio/hugo/markup/internal/attributes"
    34  )
    35  
    36  // Markdown attributes used by the Chroma hightlighter.
    37  var chromaHightlightProcessingAttributes = map[string]bool{
    38  	"anchorLineNos":      true,
    39  	"guessSyntax":        true,
    40  	"hl_Lines":           true,
    41  	"lineAnchors":        true,
    42  	"lineNos":            true,
    43  	"lineNoStart":        true,
    44  	"lineNumbersInTable": true,
    45  	"noClasses":          true,
    46  	"style":              true,
    47  	"tabWidth":           true,
    48  }
    49  
    50  func init() {
    51  	for k, v := range chromaHightlightProcessingAttributes {
    52  		chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
    53  	}
    54  }
    55  
    56  func New(cfg Config) Highlighter {
    57  	return chromaHighlighter{
    58  		cfg: cfg,
    59  	}
    60  }
    61  
    62  type Highlighter interface {
    63  	Highlight(code, lang string, opts any) (string, error)
    64  	HighlightCodeBlock(ctx hooks.CodeblockContext, opts any) (HightlightResult, error)
    65  	hooks.CodeBlockRenderer
    66  	hooks.IsDefaultCodeBlockRendererProvider
    67  }
    68  
    69  type chromaHighlighter struct {
    70  	cfg Config
    71  }
    72  
    73  func (h chromaHighlighter) Highlight(code, lang string, opts any) (string, error) {
    74  	cfg := h.cfg
    75  	if err := applyOptions(opts, &cfg); err != nil {
    76  		return "", err
    77  	}
    78  	var b strings.Builder
    79  
    80  	if _, _, err := highlight(&b, code, lang, nil, cfg); err != nil {
    81  		return "", err
    82  	}
    83  
    84  	return b.String(), nil
    85  }
    86  
    87  func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts any) (HightlightResult, error) {
    88  	cfg := h.cfg
    89  
    90  	var b strings.Builder
    91  
    92  	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
    93  
    94  	options := ctx.Options()
    95  
    96  	if err := applyOptionsFromMap(options, &cfg); err != nil {
    97  		return HightlightResult{}, err
    98  	}
    99  
   100  	// Apply these last so the user can override them.
   101  	if err := applyOptions(opts, &cfg); err != nil {
   102  		return HightlightResult{}, err
   103  	}
   104  
   105  	if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil {
   106  		return HightlightResult{}, err
   107  	}
   108  
   109  	low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg)
   110  	if err != nil {
   111  		return HightlightResult{}, err
   112  	}
   113  
   114  	highlighted := b.String()
   115  	if high == 0 {
   116  		high = len(highlighted)
   117  	}
   118  
   119  	return HightlightResult{
   120  		highlighted: template.HTML(highlighted),
   121  		innerLow:    low,
   122  		innerHigh:   high,
   123  	}, nil
   124  }
   125  
   126  func (h chromaHighlighter) RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
   127  	cfg := h.cfg
   128  
   129  	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
   130  
   131  	if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
   132  		return err
   133  	}
   134  
   135  	if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil {
   136  		return err
   137  	}
   138  
   139  	code := text.Puts(ctx.Inner())
   140  
   141  	_, _, err := highlight(w, code, ctx.Type(), attributes, cfg)
   142  	return err
   143  }
   144  
   145  func (h chromaHighlighter) IsDefaultCodeBlockRenderer() bool {
   146  	return true
   147  }
   148  
   149  var id = identity.NewPathIdentity("chroma", "highlight")
   150  
   151  func (h chromaHighlighter) GetIdentity() identity.Identity {
   152  	return id
   153  }
   154  
   155  type HightlightResult struct {
   156  	innerLow    int
   157  	innerHigh   int
   158  	highlighted template.HTML
   159  }
   160  
   161  // Wrapped returns the highlighted code wrapped in a <div>, <pre> and <code> tag.
   162  func (h HightlightResult) Wrapped() template.HTML {
   163  	return h.highlighted
   164  }
   165  
   166  // Inner returns the highlighted code without the wrapping <div>, <pre> and <code> tag, suitable for inline use.
   167  func (h HightlightResult) Inner() template.HTML {
   168  	return h.highlighted[h.innerLow:h.innerHigh]
   169  }
   170  
   171  func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) (int, int, error) {
   172  	var lexer chroma.Lexer
   173  	if lang != "" {
   174  		lexer = chromalexers.Get(lang)
   175  	}
   176  
   177  	if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
   178  		lexer = lexers.Analyse(code)
   179  		if lexer == nil {
   180  			lexer = lexers.Fallback
   181  		}
   182  		lang = strings.ToLower(lexer.Config().Name)
   183  	}
   184  
   185  	w := &byteCountFlexiWriter{delegate: fw}
   186  
   187  	if lexer == nil {
   188  		if cfg.Hl_inline {
   189  			fmt.Fprint(w, fmt.Sprintf("<code%s>%s</code>", inlineCodeAttrs(lang), gohtml.EscapeString(code)))
   190  		} else {
   191  			preWrapper := getPreWrapper(lang, w)
   192  			fmt.Fprint(w, preWrapper.Start(true, ""))
   193  			fmt.Fprint(w, gohtml.EscapeString(code))
   194  			fmt.Fprint(w, preWrapper.End(true))
   195  		}
   196  		return 0, 0, nil
   197  	}
   198  
   199  	style := styles.Get(cfg.Style)
   200  	if style == nil {
   201  		style = styles.Fallback
   202  	}
   203  	lexer = chroma.Coalesce(lexer)
   204  
   205  	iterator, err := lexer.Tokenise(nil, code)
   206  	if err != nil {
   207  		return 0, 0, err
   208  	}
   209  
   210  	if !cfg.Hl_inline {
   211  		writeDivStart(w, attributes)
   212  	}
   213  
   214  	options := cfg.ToHTMLOptions()
   215  	var wrapper html.PreWrapper
   216  
   217  	if cfg.Hl_inline {
   218  		wrapper = startEnd{
   219  			start: func(code bool, styleAttr string) string {
   220  				if code {
   221  					return fmt.Sprintf(`<code%s>`, inlineCodeAttrs(lang))
   222  				}
   223  				return ``
   224  			},
   225  			end: func(code bool) string {
   226  				if code {
   227  					return `</code>`
   228  				}
   229  
   230  				return ``
   231  			},
   232  		}
   233  
   234  	} else {
   235  		wrapper = getPreWrapper(lang, w)
   236  	}
   237  
   238  	options = append(options, html.WithPreWrapper(wrapper))
   239  
   240  	formatter := html.New(options...)
   241  
   242  	if err := formatter.Format(w, style, iterator); err != nil {
   243  		return 0, 0, err
   244  	}
   245  
   246  	if !cfg.Hl_inline {
   247  		writeDivEnd(w)
   248  	}
   249  
   250  	if p, ok := wrapper.(*preWrapper); ok {
   251  		return p.low, p.high, nil
   252  	}
   253  
   254  	return 0, 0, nil
   255  }
   256  
   257  func getPreWrapper(language string, writeCounter *byteCountFlexiWriter) *preWrapper {
   258  	return &preWrapper{language: language, writeCounter: writeCounter}
   259  }
   260  
   261  type preWrapper struct {
   262  	low          int
   263  	high         int
   264  	writeCounter *byteCountFlexiWriter
   265  	language     string
   266  }
   267  
   268  func (p *preWrapper) Start(code bool, styleAttr string) string {
   269  	var language string
   270  	if code {
   271  		language = p.language
   272  	}
   273  	w := &strings.Builder{}
   274  	WritePreStart(w, language, styleAttr)
   275  	p.low = p.writeCounter.counter + w.Len()
   276  	return w.String()
   277  }
   278  
   279  func inlineCodeAttrs(lang string) string {
   280  	if lang == "" {
   281  	}
   282  	return fmt.Sprintf(` class="code-inline language-%s"`, lang)
   283  }
   284  
   285  func WritePreStart(w io.Writer, language, styleAttr string) {
   286  	fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr)
   287  	fmt.Fprint(w, "<code")
   288  	if language != "" {
   289  		fmt.Fprint(w, ` class="language-`+language+`"`)
   290  		fmt.Fprint(w, ` data-lang="`+language+`"`)
   291  	}
   292  	fmt.Fprint(w, ">")
   293  }
   294  
   295  const preEnd = "</code></pre>"
   296  
   297  func (p *preWrapper) End(code bool) string {
   298  	p.high = p.writeCounter.counter
   299  	return preEnd
   300  }
   301  
   302  type startEnd struct {
   303  	start func(code bool, styleAttr string) string
   304  	end   func(code bool) string
   305  }
   306  
   307  func (s startEnd) Start(code bool, styleAttr string) string {
   308  	return s.start(code, styleAttr)
   309  }
   310  
   311  func (s startEnd) End(code bool) string {
   312  	return s.end(code)
   313  }
   314  
   315  func WritePreEnd(w io.Writer) {
   316  	fmt.Fprint(w, preEnd)
   317  }
   318  
   319  func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
   320  	w.WriteString(`<div class="highlight`)
   321  	if attrs != nil {
   322  		for _, attr := range attrs {
   323  			if attr.Name == "class" {
   324  				w.WriteString(" " + attr.ValueString())
   325  				break
   326  			}
   327  		}
   328  		_, _ = w.WriteString("\"")
   329  		attributes.RenderAttributes(w, true, attrs...)
   330  	} else {
   331  		_, _ = w.WriteString("\"")
   332  	}
   333  
   334  	w.WriteString(">")
   335  }
   336  
   337  func writeDivEnd(w hugio.FlexiWriter) {
   338  	w.WriteString("</div>")
   339  }
   340  
   341  type byteCountFlexiWriter struct {
   342  	delegate hugio.FlexiWriter
   343  	counter  int
   344  }
   345  
   346  func (w *byteCountFlexiWriter) Write(p []byte) (int, error) {
   347  	n, err := w.delegate.Write(p)
   348  	w.counter += n
   349  	return n, err
   350  }
   351  
   352  func (w *byteCountFlexiWriter) WriteByte(c byte) error {
   353  	w.counter++
   354  	return w.delegate.WriteByte(c)
   355  }
   356  
   357  func (w *byteCountFlexiWriter) WriteString(s string) (int, error) {
   358  	n, err := w.delegate.WriteString(s)
   359  	w.counter += n
   360  	return n, err
   361  }
   362  
   363  func (w *byteCountFlexiWriter) WriteRune(r rune) (int, error) {
   364  	n, err := w.delegate.WriteRune(r)
   365  	w.counter += n
   366  	return n, err
   367  }