github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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 "io" 20 "strings" 21 22 "github.com/alecthomas/chroma" 23 "github.com/alecthomas/chroma/formatters/html" 24 "github.com/alecthomas/chroma/lexers" 25 "github.com/alecthomas/chroma/styles" 26 hl "github.com/yuin/goldmark-highlighting" 27 ) 28 29 func New(cfg Config) Highlighter { 30 return Highlighter{ 31 cfg: cfg, 32 } 33 } 34 35 type Highlighter struct { 36 cfg Config 37 } 38 39 func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) { 40 if optsStr == "" { 41 return highlight(code, lang, h.cfg) 42 } 43 44 cfg := h.cfg 45 if err := applyOptionsFromString(optsStr, &cfg); err != nil { 46 return "", err 47 } 48 49 return highlight(code, lang, cfg) 50 } 51 52 func highlight(code, lang string, cfg Config) (string, error) { 53 w := &strings.Builder{} 54 var lexer chroma.Lexer 55 if lang != "" { 56 lexer = lexers.Get(lang) 57 } 58 59 if lexer == nil && cfg.GuessSyntax { 60 lexer = lexers.Analyse(code) 61 if lexer == nil { 62 lexer = lexers.Fallback 63 } 64 lang = strings.ToLower(lexer.Config().Name) 65 } 66 67 if lexer == nil { 68 wrapper := getPreWrapper(lang) 69 fmt.Fprint(w, wrapper.Start(true, "")) 70 fmt.Fprint(w, gohtml.EscapeString(code)) 71 fmt.Fprint(w, wrapper.End(true)) 72 return w.String(), nil 73 } 74 75 style := styles.Get(cfg.Style) 76 if style == nil { 77 style = styles.Fallback 78 } 79 lexer = chroma.Coalesce(lexer) 80 81 iterator, err := lexer.Tokenise(nil, code) 82 if err != nil { 83 return "", err 84 } 85 86 options := cfg.ToHTMLOptions() 87 options = append(options, getHtmlPreWrapper(lang)) 88 89 formatter := html.New(options...) 90 91 fmt.Fprint(w, `<div class="highlight">`) 92 if err := formatter.Format(w, style, iterator); err != nil { 93 return "", err 94 } 95 fmt.Fprint(w, `</div>`) 96 97 return w.String(), nil 98 } 99 100 func GetCodeBlockOptions() func(ctx hl.CodeBlockContext) []html.Option { 101 return func(ctx hl.CodeBlockContext) []html.Option { 102 var language string 103 if l, ok := ctx.Language(); ok { 104 language = string(l) 105 } 106 return []html.Option{ 107 getHtmlPreWrapper(language), 108 } 109 } 110 } 111 112 func getPreWrapper(language string) preWrapper { 113 return preWrapper{language: language} 114 } 115 116 func getHtmlPreWrapper(language string) html.Option { 117 return html.WithPreWrapper(getPreWrapper(language)) 118 } 119 120 type preWrapper struct { 121 language string 122 } 123 124 func (p preWrapper) Start(code bool, styleAttr string) string { 125 var language string 126 if code { 127 language = p.language 128 } 129 w := &strings.Builder{} 130 WritePreStart(w, language, styleAttr) 131 return w.String() 132 } 133 134 func WritePreStart(w io.Writer, language, styleAttr string) { 135 fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr) 136 fmt.Fprint(w, "<code") 137 if language != "" { 138 fmt.Fprint(w, ` class="language-`+language+`"`) 139 fmt.Fprint(w, ` data-lang="`+language+`"`) 140 } 141 fmt.Fprint(w, ">") 142 } 143 144 const preEnd = "</code></pre>" 145 146 func (p preWrapper) End(code bool) string { 147 return preEnd 148 } 149 150 func WritePreEnd(w io.Writer) { 151 fmt.Fprint(w, preEnd) 152 }