github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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 "html/template" 20 "io" 21 "strconv" 22 "strings" 23 24 "github.com/alecthomas/chroma" 25 "github.com/alecthomas/chroma/formatters/html" 26 "github.com/alecthomas/chroma/lexers" 27 "github.com/alecthomas/chroma/styles" 28 "github.com/gohugoio/hugo/common/hugio" 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 interface{}) (string, error) 62 HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) 63 hooks.CodeBlockRenderer 64 } 65 66 type chromaHighlighter struct { 67 cfg Config 68 } 69 70 func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) { 71 cfg := h.cfg 72 if err := applyOptions(opts, &cfg); err != nil { 73 return "", err 74 } 75 var b strings.Builder 76 77 if err := highlight(&b, code, lang, nil, cfg); err != nil { 78 return "", err 79 } 80 81 return b.String(), nil 82 } 83 84 func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) { 85 cfg := h.cfg 86 87 var b strings.Builder 88 89 attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice() 90 options := ctx.Options() 91 92 if err := applyOptionsFromMap(options, &cfg); err != nil { 93 return HightlightResult{}, err 94 } 95 96 // Apply these last so the user can override them. 97 if err := applyOptions(opts, &cfg); err != nil { 98 return HightlightResult{}, err 99 } 100 101 err := highlight(&b, ctx.Code(), ctx.Lang(), attributes, cfg) 102 if err != nil { 103 return HightlightResult{}, err 104 } 105 106 return HightlightResult{ 107 Body: template.HTML(b.String()), 108 }, nil 109 } 110 111 func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error { 112 cfg := h.cfg 113 attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice() 114 115 if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil { 116 return err 117 } 118 119 return highlight(w, ctx.Code(), ctx.Lang(), attributes, cfg) 120 } 121 122 var id = identity.NewPathIdentity("chroma", "highlight") 123 124 func (h chromaHighlighter) GetIdentity() identity.Identity { 125 return id 126 } 127 128 type HightlightResult struct { 129 Body template.HTML 130 } 131 132 func (h HightlightResult) Highlighted() template.HTML { 133 return h.Body 134 } 135 136 func (h chromaHighlighter) toHighlightOptionsAttributes(ctx hooks.CodeblockContext) (map[string]interface{}, map[string]interface{}) { 137 attributes := ctx.Attributes() 138 if attributes == nil || len(attributes) == 0 { 139 return nil, nil 140 } 141 142 options := make(map[string]interface{}) 143 attrs := make(map[string]interface{}) 144 145 for k, v := range attributes { 146 klow := strings.ToLower(k) 147 if chromaHightlightProcessingAttributes[klow] { 148 options[klow] = v 149 } else { 150 attrs[k] = v 151 } 152 } 153 const lineanchorsKey = "lineanchors" 154 if _, found := options[lineanchorsKey]; !found { 155 // Set it to the ordinal. 156 options[lineanchorsKey] = strconv.Itoa(ctx.Ordinal()) 157 } 158 return options, attrs 159 } 160 161 func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error { 162 var lexer chroma.Lexer 163 if lang != "" { 164 lexer = lexers.Get(lang) 165 } 166 167 if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) { 168 lexer = lexers.Analyse(code) 169 if lexer == nil { 170 lexer = lexers.Fallback 171 } 172 lang = strings.ToLower(lexer.Config().Name) 173 } 174 175 if lexer == nil { 176 wrapper := getPreWrapper(lang) 177 fmt.Fprint(w, wrapper.Start(true, "")) 178 fmt.Fprint(w, gohtml.EscapeString(code)) 179 fmt.Fprint(w, wrapper.End(true)) 180 return nil 181 } 182 183 style := styles.Get(cfg.Style) 184 if style == nil { 185 style = styles.Fallback 186 } 187 lexer = chroma.Coalesce(lexer) 188 189 iterator, err := lexer.Tokenise(nil, code) 190 if err != nil { 191 return err 192 } 193 194 options := cfg.ToHTMLOptions() 195 options = append(options, getHtmlPreWrapper(lang)) 196 197 formatter := html.New(options...) 198 199 writeDivStart(w, attributes) 200 if err := formatter.Format(w, style, iterator); err != nil { 201 return err 202 } 203 writeDivEnd(w) 204 205 return nil 206 } 207 208 func getPreWrapper(language string) preWrapper { 209 return preWrapper{language: language} 210 } 211 212 func getHtmlPreWrapper(language string) html.Option { 213 return html.WithPreWrapper(getPreWrapper(language)) 214 } 215 216 type preWrapper struct { 217 language string 218 } 219 220 func (p preWrapper) Start(code bool, styleAttr string) string { 221 var language string 222 if code { 223 language = p.language 224 } 225 w := &strings.Builder{} 226 WritePreStart(w, language, styleAttr) 227 return w.String() 228 } 229 230 func WritePreStart(w io.Writer, language, styleAttr string) { 231 fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr) 232 fmt.Fprint(w, "<code") 233 if language != "" { 234 fmt.Fprint(w, ` class="language-`+language+`"`) 235 fmt.Fprint(w, ` data-lang="`+language+`"`) 236 } 237 fmt.Fprint(w, ">") 238 } 239 240 const preEnd = "</code></pre>" 241 242 func (p preWrapper) End(code bool) string { 243 return preEnd 244 } 245 246 func WritePreEnd(w io.Writer) { 247 fmt.Fprint(w, preEnd) 248 } 249 250 func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) { 251 w.WriteString(`<div class="highlight`) 252 if attrs != nil { 253 for _, attr := range attrs { 254 if attr.Name == "class" { 255 w.WriteString(" " + attr.ValueString()) 256 break 257 } 258 } 259 _, _ = w.WriteString("\"") 260 attributes.RenderAttributes(w, true, attrs...) 261 } else { 262 _, _ = w.WriteString("\"") 263 } 264 265 w.WriteString(">") 266 } 267 268 func writeDivEnd(w hugio.FlexiWriter) { 269 w.WriteString("</div>") 270 }