hugo/markup/highlight/highlight.go

153 lines
3.4 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package highlight
import (
"fmt"
gohtml "html"
"io"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
hl "github.com/yuin/goldmark-highlighting"
)
func New(cfg Config) Highlighter {
return Highlighter{
cfg: cfg,
}
}
type Highlighter struct {
cfg Config
}
func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
if optsStr == "" {
return highlight(code, lang, h.cfg)
}
cfg := h.cfg
if err := applyOptionsFromString(optsStr, &cfg); err != nil {
return "", err
}
return highlight(code, lang, cfg)
}
func highlight(code, lang string, cfg Config) (string, error) {
w := &strings.Builder{}
var lexer chroma.Lexer
if lang != "" {
lexer = lexers.Get(lang)
}
if lexer == nil && cfg.GuessSyntax {
lexer = lexers.Analyse(code)
if lexer == nil {
lexer = lexers.Fallback
}
lang = strings.ToLower(lexer.Config().Name)
}
if lexer == nil {
wrapper := getPreWrapper(lang)
fmt.Fprint(w, wrapper.Start(true, ""))
fmt.Fprint(w, gohtml.EscapeString(code))
fmt.Fprint(w, wrapper.End(true))
return w.String(), nil
}
style := styles.Get(cfg.Style)
if style == nil {
style = styles.Fallback
}
lexer = chroma.Coalesce(lexer)
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
return "", err
}
options := cfg.ToHTMLOptions()
options = append(options, getHtmlPreWrapper(lang))
formatter := html.New(options...)
fmt.Fprint(w, `<div class="highlight">`)
if err := formatter.Format(w, style, iterator); err != nil {
return "", err
}
fmt.Fprint(w, `</div>`)
return w.String(), nil
}
func GetCodeBlockOptions() func(ctx hl.CodeBlockContext) []html.Option {
return func(ctx hl.CodeBlockContext) []html.Option {
var language string
if l, ok := ctx.Language(); ok {
language = string(l)
}
return []html.Option{
getHtmlPreWrapper(language),
}
}
}
func getPreWrapper(language string) preWrapper {
return preWrapper{language: language}
}
func getHtmlPreWrapper(language string) html.Option {
return html.WithPreWrapper(getPreWrapper(language))
}
type preWrapper struct {
language string
}
func (p preWrapper) Start(code bool, styleAttr string) string {
var language string
if code {
language = p.language
}
w := &strings.Builder{}
WritePreStart(w, language, styleAttr)
return w.String()
}
func WritePreStart(w io.Writer, language, styleAttr string) {
fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr)
fmt.Fprint(w, "<code")
if language != "" {
fmt.Fprint(w, ` class="language-`+language+`"`)
fmt.Fprint(w, ` data-lang="`+language+`"`)
}
fmt.Fprint(w, ">")
}
const preEnd = "</code></pre>"
func (p preWrapper) End(code bool) string {
return preEnd
}
func WritePreEnd(w io.Writer) {
fmt.Fprint(w, preEnd)
}