hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

highlight.go (8371B)

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