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