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 }