github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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 highlighter.
    37  var chromaHighlightProcessingAttributes = 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 chromaHighlightProcessingAttributes {
    52  		chromaHighlightProcessingAttributes[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) (HighlightResult, 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) (HighlightResult, 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 HighlightResult{}, err
    98  	}
    99  
   100  	// Apply these last so the user can override them.
   101  	if err := applyOptions(opts, &cfg); err != nil {
   102  		return HighlightResult{}, err
   103  	}
   104  
   105  	if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil {
   106  		return HighlightResult{}, err
   107  	}
   108  
   109  	low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg)
   110  	if err != nil {
   111  		return HighlightResult{}, err
   112  	}
   113  
   114  	highlighted := b.String()
   115  	if high == 0 {
   116  		high = len(highlighted)
   117  	}
   118  
   119  	return HighlightResult{
   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  // GetIdentity is for internal use.
   152  func (h chromaHighlighter) GetIdentity() identity.Identity {
   153  	return id
   154  }
   155  
   156  // HighlightResult holds the result of an highlighting operation.
   157  type HighlightResult struct {
   158  	innerLow    int
   159  	innerHigh   int
   160  	highlighted template.HTML
   161  }
   162  
   163  // Wrapped returns the highlighted code wrapped in a <div>, <pre> and <code> tag.
   164  func (h HighlightResult) Wrapped() template.HTML {
   165  	return h.highlighted
   166  }
   167  
   168  // Inner returns the highlighted code without the wrapping <div>, <pre> and <code> tag, suitable for inline use.
   169  func (h HighlightResult) Inner() template.HTML {
   170  	return h.highlighted[h.innerLow:h.innerHigh]
   171  }
   172  
   173  func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) (int, int, error) {
   174  	var lexer chroma.Lexer
   175  	if lang != "" {
   176  		lexer = chromalexers.Get(lang)
   177  	}
   178  
   179  	if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
   180  		lexer = lexers.Analyse(code)
   181  		if lexer == nil {
   182  			lexer = lexers.Fallback
   183  		}
   184  		lang = strings.ToLower(lexer.Config().Name)
   185  	}
   186  
   187  	w := &byteCountFlexiWriter{delegate: fw}
   188  
   189  	if lexer == nil {
   190  		if cfg.Hl_inline {
   191  			fmt.Fprint(w, fmt.Sprintf("<code%s>%s</code>", inlineCodeAttrs(lang), gohtml.EscapeString(code)))
   192  		} else {
   193  			preWrapper := getPreWrapper(lang, w)
   194  			fmt.Fprint(w, preWrapper.Start(true, ""))
   195  			fmt.Fprint(w, gohtml.EscapeString(code))
   196  			fmt.Fprint(w, preWrapper.End(true))
   197  		}
   198  		return 0, 0, nil
   199  	}
   200  
   201  	style := styles.Get(cfg.Style)
   202  	if style == nil {
   203  		style = styles.Fallback
   204  	}
   205  	lexer = chroma.Coalesce(lexer)
   206  
   207  	iterator, err := lexer.Tokenise(nil, code)
   208  	if err != nil {
   209  		return 0, 0, err
   210  	}
   211  
   212  	if !cfg.Hl_inline {
   213  		writeDivStart(w, attributes)
   214  	}
   215  
   216  	options := cfg.toHTMLOptions()
   217  	var wrapper html.PreWrapper
   218  
   219  	if cfg.Hl_inline {
   220  		wrapper = startEnd{
   221  			start: func(code bool, styleAttr string) string {
   222  				if code {
   223  					return fmt.Sprintf(`<code%s>`, inlineCodeAttrs(lang))
   224  				}
   225  				return ``
   226  			},
   227  			end: func(code bool) string {
   228  				if code {
   229  					return `</code>`
   230  				}
   231  
   232  				return ``
   233  			},
   234  		}
   235  
   236  	} else {
   237  		wrapper = getPreWrapper(lang, w)
   238  	}
   239  
   240  	options = append(options, html.WithPreWrapper(wrapper))
   241  
   242  	formatter := html.New(options...)
   243  
   244  	if err := formatter.Format(w, style, iterator); err != nil {
   245  		return 0, 0, err
   246  	}
   247  
   248  	if !cfg.Hl_inline {
   249  		writeDivEnd(w)
   250  	}
   251  
   252  	if p, ok := wrapper.(*preWrapper); ok {
   253  		return p.low, p.high, nil
   254  	}
   255  
   256  	return 0, 0, nil
   257  }
   258  
   259  func getPreWrapper(language string, writeCounter *byteCountFlexiWriter) *preWrapper {
   260  	return &preWrapper{language: language, writeCounter: writeCounter}
   261  }
   262  
   263  type preWrapper struct {
   264  	low          int
   265  	high         int
   266  	writeCounter *byteCountFlexiWriter
   267  	language     string
   268  }
   269  
   270  func (p *preWrapper) Start(code bool, styleAttr string) string {
   271  	var language string
   272  	if code {
   273  		language = p.language
   274  	}
   275  	w := &strings.Builder{}
   276  	WritePreStart(w, language, styleAttr)
   277  	p.low = p.writeCounter.counter + w.Len()
   278  	return w.String()
   279  }
   280  
   281  func inlineCodeAttrs(lang string) string {
   282  	if lang == "" {
   283  	}
   284  	return fmt.Sprintf(` class="code-inline language-%s"`, lang)
   285  }
   286  
   287  func WritePreStart(w io.Writer, language, styleAttr string) {
   288  	fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr)
   289  	fmt.Fprint(w, "<code")
   290  	if language != "" {
   291  		fmt.Fprint(w, ` class="language-`+language+`"`)
   292  		fmt.Fprint(w, ` data-lang="`+language+`"`)
   293  	}
   294  	fmt.Fprint(w, ">")
   295  }
   296  
   297  const preEnd = "</code></pre>"
   298  
   299  func (p *preWrapper) End(code bool) string {
   300  	p.high = p.writeCounter.counter
   301  	return preEnd
   302  }
   303  
   304  type startEnd struct {
   305  	start func(code bool, styleAttr string) string
   306  	end   func(code bool) string
   307  }
   308  
   309  func (s startEnd) Start(code bool, styleAttr string) string {
   310  	return s.start(code, styleAttr)
   311  }
   312  
   313  func (s startEnd) End(code bool) string {
   314  	return s.end(code)
   315  }
   316  
   317  func WritePreEnd(w io.Writer) {
   318  	fmt.Fprint(w, preEnd)
   319  }
   320  
   321  func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
   322  	w.WriteString(`<div class="highlight`)
   323  	if attrs != nil {
   324  		for _, attr := range attrs {
   325  			if attr.Name == "class" {
   326  				w.WriteString(" " + attr.ValueString())
   327  				break
   328  			}
   329  		}
   330  		_, _ = w.WriteString("\"")
   331  		attributes.RenderAttributes(w, true, attrs...)
   332  	} else {
   333  		_, _ = w.WriteString("\"")
   334  	}
   335  
   336  	w.WriteString(">")
   337  }
   338  
   339  func writeDivEnd(w hugio.FlexiWriter) {
   340  	w.WriteString("</div>")
   341  }
   342  
   343  type byteCountFlexiWriter struct {
   344  	delegate hugio.FlexiWriter
   345  	counter  int
   346  }
   347  
   348  func (w *byteCountFlexiWriter) Write(p []byte) (int, error) {
   349  	n, err := w.delegate.Write(p)
   350  	w.counter += n
   351  	return n, err
   352  }
   353  
   354  func (w *byteCountFlexiWriter) WriteByte(c byte) error {
   355  	w.counter++
   356  	return w.delegate.WriteByte(c)
   357  }
   358  
   359  func (w *byteCountFlexiWriter) WriteString(s string) (int, error) {
   360  	n, err := w.delegate.WriteString(s)
   361  	w.counter += n
   362  	return n, err
   363  }
   364  
   365  func (w *byteCountFlexiWriter) WriteRune(r rune) (int, error) {
   366  	n, err := w.delegate.WriteRune(r)
   367  	w.counter += n
   368  	return n, err
   369  }