github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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 highlighter. 37 var chromaHighlightProcessingAttributes = 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 chromaHighlightProcessingAttributes { 52 chromaHighlightProcessingAttributes[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) (HighlightResult, 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) (HighlightResult, 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 HighlightResult{}, err 98 } 99 100 // Apply these last so the user can override them. 101 if err := applyOptions(opts, &cfg); err != nil { 102 return HighlightResult{}, err 103 } 104 105 if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil { 106 return HighlightResult{}, err 107 } 108 109 low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg) 110 if err != nil { 111 return HighlightResult{}, err 112 } 113 114 highlighted := b.String() 115 if high == 0 { 116 high = len(highlighted) 117 } 118 119 return HighlightResult{ 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 // GetIdentity is for internal use. 152 func (h chromaHighlighter) GetIdentity() identity.Identity { 153 return id 154 } 155 156 // HighlightResult holds the result of an highlighting operation. 157 type HighlightResult struct { 158 innerLow int 159 innerHigh int 160 highlighted template.HTML 161 } 162 163 // Wrapped returns the highlighted code wrapped in a <div>, <pre> and <code> tag. 164 func (h HighlightResult) Wrapped() template.HTML { 165 return h.highlighted 166 } 167 168 // Inner returns the highlighted code without the wrapping <div>, <pre> and <code> tag, suitable for inline use. 169 func (h HighlightResult) Inner() template.HTML { 170 return h.highlighted[h.innerLow:h.innerHigh] 171 } 172 173 func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) (int, int, error) { 174 var lexer chroma.Lexer 175 if lang != "" { 176 lexer = chromalexers.Get(lang) 177 } 178 179 if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) { 180 lexer = lexers.Analyse(code) 181 if lexer == nil { 182 lexer = lexers.Fallback 183 } 184 lang = strings.ToLower(lexer.Config().Name) 185 } 186 187 w := &byteCountFlexiWriter{delegate: fw} 188 189 if lexer == nil { 190 if cfg.Hl_inline { 191 fmt.Fprint(w, fmt.Sprintf("<code%s>%s</code>", inlineCodeAttrs(lang), gohtml.EscapeString(code))) 192 } else { 193 preWrapper := getPreWrapper(lang, w) 194 fmt.Fprint(w, preWrapper.Start(true, "")) 195 fmt.Fprint(w, gohtml.EscapeString(code)) 196 fmt.Fprint(w, preWrapper.End(true)) 197 } 198 return 0, 0, nil 199 } 200 201 style := styles.Get(cfg.Style) 202 if style == nil { 203 style = styles.Fallback 204 } 205 lexer = chroma.Coalesce(lexer) 206 207 iterator, err := lexer.Tokenise(nil, code) 208 if err != nil { 209 return 0, 0, err 210 } 211 212 if !cfg.Hl_inline { 213 writeDivStart(w, attributes) 214 } 215 216 options := cfg.toHTMLOptions() 217 var wrapper html.PreWrapper 218 219 if cfg.Hl_inline { 220 wrapper = startEnd{ 221 start: func(code bool, styleAttr string) string { 222 if code { 223 return fmt.Sprintf(`<code%s>`, inlineCodeAttrs(lang)) 224 } 225 return `` 226 }, 227 end: func(code bool) string { 228 if code { 229 return `</code>` 230 } 231 232 return `` 233 }, 234 } 235 236 } else { 237 wrapper = getPreWrapper(lang, w) 238 } 239 240 options = append(options, html.WithPreWrapper(wrapper)) 241 242 formatter := html.New(options...) 243 244 if err := formatter.Format(w, style, iterator); err != nil { 245 return 0, 0, err 246 } 247 248 if !cfg.Hl_inline { 249 writeDivEnd(w) 250 } 251 252 if p, ok := wrapper.(*preWrapper); ok { 253 return p.low, p.high, nil 254 } 255 256 return 0, 0, nil 257 } 258 259 func getPreWrapper(language string, writeCounter *byteCountFlexiWriter) *preWrapper { 260 return &preWrapper{language: language, writeCounter: writeCounter} 261 } 262 263 type preWrapper struct { 264 low int 265 high int 266 writeCounter *byteCountFlexiWriter 267 language string 268 } 269 270 func (p *preWrapper) Start(code bool, styleAttr string) string { 271 var language string 272 if code { 273 language = p.language 274 } 275 w := &strings.Builder{} 276 WritePreStart(w, language, styleAttr) 277 p.low = p.writeCounter.counter + w.Len() 278 return w.String() 279 } 280 281 func inlineCodeAttrs(lang string) string { 282 if lang == "" { 283 } 284 return fmt.Sprintf(` class="code-inline language-%s"`, lang) 285 } 286 287 func WritePreStart(w io.Writer, language, styleAttr string) { 288 fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr) 289 fmt.Fprint(w, "<code") 290 if language != "" { 291 fmt.Fprint(w, ` class="language-`+language+`"`) 292 fmt.Fprint(w, ` data-lang="`+language+`"`) 293 } 294 fmt.Fprint(w, ">") 295 } 296 297 const preEnd = "</code></pre>" 298 299 func (p *preWrapper) End(code bool) string { 300 p.high = p.writeCounter.counter 301 return preEnd 302 } 303 304 type startEnd struct { 305 start func(code bool, styleAttr string) string 306 end func(code bool) string 307 } 308 309 func (s startEnd) Start(code bool, styleAttr string) string { 310 return s.start(code, styleAttr) 311 } 312 313 func (s startEnd) End(code bool) string { 314 return s.end(code) 315 } 316 317 func WritePreEnd(w io.Writer) { 318 fmt.Fprint(w, preEnd) 319 } 320 321 func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) { 322 w.WriteString(`<div class="highlight`) 323 if attrs != nil { 324 for _, attr := range attrs { 325 if attr.Name == "class" { 326 w.WriteString(" " + attr.ValueString()) 327 break 328 } 329 } 330 _, _ = w.WriteString("\"") 331 attributes.RenderAttributes(w, true, attrs...) 332 } else { 333 _, _ = w.WriteString("\"") 334 } 335 336 w.WriteString(">") 337 } 338 339 func writeDivEnd(w hugio.FlexiWriter) { 340 w.WriteString("</div>") 341 } 342 343 type byteCountFlexiWriter struct { 344 delegate hugio.FlexiWriter 345 counter int 346 } 347 348 func (w *byteCountFlexiWriter) Write(p []byte) (int, error) { 349 n, err := w.delegate.Write(p) 350 w.counter += n 351 return n, err 352 } 353 354 func (w *byteCountFlexiWriter) WriteByte(c byte) error { 355 w.counter++ 356 return w.delegate.WriteByte(c) 357 } 358 359 func (w *byteCountFlexiWriter) WriteString(s string) (int, error) { 360 n, err := w.delegate.WriteString(s) 361 w.counter += n 362 return n, err 363 } 364 365 func (w *byteCountFlexiWriter) WriteRune(r rune) (int, error) { 366 n, err := w.delegate.WriteRune(r) 367 w.counter += n 368 return n, err 369 }