github.com/schumacherfm/hugo@v0.47.1/helpers/pygments.go (about) 1 // Copyright 2016 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 helpers 15 16 import ( 17 "bytes" 18 "crypto/sha1" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "os/exec" 23 "path/filepath" 24 "regexp" 25 "sort" 26 "strconv" 27 "strings" 28 29 "github.com/alecthomas/chroma" 30 "github.com/alecthomas/chroma/formatters" 31 "github.com/alecthomas/chroma/formatters/html" 32 "github.com/alecthomas/chroma/lexers" 33 "github.com/alecthomas/chroma/styles" 34 bp "github.com/gohugoio/hugo/bufferpool" 35 36 "github.com/gohugoio/hugo/config" 37 "github.com/gohugoio/hugo/hugofs" 38 jww "github.com/spf13/jwalterweatherman" 39 ) 40 41 const pygmentsBin = "pygmentize" 42 43 // hasPygments checks to see if Pygments is installed and available 44 // on the system. 45 func hasPygments() bool { 46 if _, err := exec.LookPath(pygmentsBin); err != nil { 47 return false 48 } 49 return true 50 } 51 52 type highlighters struct { 53 cs *ContentSpec 54 ignoreCache bool 55 cacheDir string 56 } 57 58 func newHiglighters(cs *ContentSpec) highlighters { 59 return highlighters{cs: cs, ignoreCache: cs.cfg.GetBool("ignoreCache"), cacheDir: cs.cfg.GetString("cacheDir")} 60 } 61 62 func (h highlighters) chromaHighlight(code, lang, optsStr string) (string, error) { 63 opts, err := h.cs.parsePygmentsOpts(optsStr) 64 if err != nil { 65 jww.ERROR.Print(err.Error()) 66 return code, err 67 } 68 69 style, found := opts["style"] 70 if !found || style == "" { 71 style = "friendly" 72 } 73 74 f, err := h.cs.chromaFormatterFromOptions(opts) 75 if err != nil { 76 jww.ERROR.Print(err.Error()) 77 return code, err 78 } 79 80 b := bp.GetBuffer() 81 defer bp.PutBuffer(b) 82 83 err = chromaHighlight(b, code, lang, style, f) 84 if err != nil { 85 jww.ERROR.Print(err.Error()) 86 return code, err 87 } 88 89 return h.injectCodeTag(`<div class="highlight">`+b.String()+"</div>", lang), nil 90 } 91 92 func (h highlighters) pygmentsHighlight(code, lang, optsStr string) (string, error) { 93 options, err := h.cs.createPygmentsOptionsString(optsStr) 94 95 if err != nil { 96 jww.ERROR.Print(err.Error()) 97 return code, nil 98 } 99 100 // Try to read from cache first 101 hash := sha1.New() 102 io.WriteString(hash, code) 103 io.WriteString(hash, lang) 104 io.WriteString(hash, options) 105 106 fs := hugofs.Os 107 108 var cachefile string 109 110 if !h.ignoreCache && h.cacheDir != "" { 111 cachefile = filepath.Join(h.cacheDir, fmt.Sprintf("pygments-%x", hash.Sum(nil))) 112 113 exists, err := Exists(cachefile, fs) 114 if err != nil { 115 jww.ERROR.Print(err.Error()) 116 return code, nil 117 } 118 if exists { 119 f, err := fs.Open(cachefile) 120 if err != nil { 121 jww.ERROR.Print(err.Error()) 122 return code, nil 123 } 124 125 s, err := ioutil.ReadAll(f) 126 if err != nil { 127 jww.ERROR.Print(err.Error()) 128 return code, nil 129 } 130 131 return string(s), nil 132 } 133 } 134 135 // No cache file, render and cache it 136 var out bytes.Buffer 137 var stderr bytes.Buffer 138 139 var langOpt string 140 if lang == "" { 141 langOpt = "-g" // Try guessing the language 142 } else { 143 langOpt = "-l" + lang 144 } 145 146 cmd := exec.Command(pygmentsBin, langOpt, "-fhtml", "-O", options) 147 cmd.Stdin = strings.NewReader(code) 148 cmd.Stdout = &out 149 cmd.Stderr = &stderr 150 151 if err := cmd.Run(); err != nil { 152 jww.ERROR.Print(stderr.String()) 153 return code, err 154 } 155 156 str := string(normalizeExternalHelperLineFeeds([]byte(out.String()))) 157 158 str = h.injectCodeTag(str, lang) 159 160 if !h.ignoreCache && cachefile != "" { 161 // Write cache file 162 if err := WriteToDisk(cachefile, strings.NewReader(str), fs); err != nil { 163 jww.ERROR.Print(stderr.String()) 164 } 165 } 166 167 return str, nil 168 } 169 170 var preRe = regexp.MustCompile(`(?s)(.*?<pre.*?>)(.*?)(</pre>)`) 171 172 func (h highlighters) injectCodeTag(code, lang string) string { 173 if lang == "" { 174 return code 175 } 176 codeTag := fmt.Sprintf(`<code class="language-%s" data-lang="%s">`, lang, lang) 177 return preRe.ReplaceAllString(code, fmt.Sprintf("$1%s$2</code>$3", codeTag)) 178 } 179 180 func chromaHighlight(w io.Writer, source, lexer, style string, f chroma.Formatter) error { 181 l := lexers.Get(lexer) 182 if l == nil { 183 l = lexers.Analyse(source) 184 } 185 if l == nil { 186 l = lexers.Fallback 187 } 188 l = chroma.Coalesce(l) 189 190 if f == nil { 191 f = formatters.Fallback 192 } 193 194 s := styles.Get(style) 195 if s == nil { 196 s = styles.Fallback 197 } 198 199 it, err := l.Tokenise(nil, source) 200 if err != nil { 201 return err 202 } 203 204 return f.Format(w, s, it) 205 } 206 207 var pygmentsKeywords = make(map[string]bool) 208 209 func init() { 210 pygmentsKeywords["encoding"] = true 211 pygmentsKeywords["outencoding"] = true 212 pygmentsKeywords["nowrap"] = true 213 pygmentsKeywords["full"] = true 214 pygmentsKeywords["title"] = true 215 pygmentsKeywords["style"] = true 216 pygmentsKeywords["noclasses"] = true 217 pygmentsKeywords["classprefix"] = true 218 pygmentsKeywords["cssclass"] = true 219 pygmentsKeywords["cssstyles"] = true 220 pygmentsKeywords["prestyles"] = true 221 pygmentsKeywords["linenos"] = true 222 pygmentsKeywords["hl_lines"] = true 223 pygmentsKeywords["linenostart"] = true 224 pygmentsKeywords["linenostep"] = true 225 pygmentsKeywords["linenospecial"] = true 226 pygmentsKeywords["nobackground"] = true 227 pygmentsKeywords["lineseparator"] = true 228 pygmentsKeywords["lineanchors"] = true 229 pygmentsKeywords["linespans"] = true 230 pygmentsKeywords["anchorlinenos"] = true 231 pygmentsKeywords["startinline"] = true 232 } 233 234 func parseOptions(defaults map[string]string, in string) (map[string]string, error) { 235 in = strings.Trim(in, " ") 236 opts := make(map[string]string) 237 238 if defaults != nil { 239 for k, v := range defaults { 240 opts[k] = v 241 } 242 } 243 244 if in == "" { 245 return opts, nil 246 } 247 248 for _, v := range strings.Split(in, ",") { 249 keyVal := strings.Split(v, "=") 250 key := strings.ToLower(strings.Trim(keyVal[0], " ")) 251 if len(keyVal) != 2 || !pygmentsKeywords[key] { 252 return opts, fmt.Errorf("invalid Pygments option: %s", key) 253 } 254 opts[key] = keyVal[1] 255 } 256 257 return opts, nil 258 } 259 260 func createOptionsString(options map[string]string) string { 261 var keys []string 262 for k := range options { 263 keys = append(keys, k) 264 } 265 sort.Strings(keys) 266 267 var optionsStr string 268 for i, k := range keys { 269 optionsStr += fmt.Sprintf("%s=%s", k, options[k]) 270 if i < len(options)-1 { 271 optionsStr += "," 272 } 273 } 274 275 return optionsStr 276 } 277 278 func parseDefaultPygmentsOpts(cfg config.Provider) (map[string]string, error) { 279 options, err := parseOptions(nil, cfg.GetString("pygmentsOptions")) 280 if err != nil { 281 return nil, err 282 } 283 284 if cfg.IsSet("pygmentsStyle") { 285 options["style"] = cfg.GetString("pygmentsStyle") 286 } 287 288 if cfg.IsSet("pygmentsUseClasses") { 289 if cfg.GetBool("pygmentsUseClasses") { 290 options["noclasses"] = "false" 291 } else { 292 options["noclasses"] = "true" 293 } 294 295 } 296 297 if _, ok := options["encoding"]; !ok { 298 options["encoding"] = "utf8" 299 } 300 301 return options, nil 302 } 303 304 func (cs *ContentSpec) chromaFormatterFromOptions(pygmentsOpts map[string]string) (chroma.Formatter, error) { 305 var options = []html.Option{html.TabWidth(4)} 306 307 if pygmentsOpts["noclasses"] == "false" { 308 options = append(options, html.WithClasses()) 309 } 310 311 lineNumbers := pygmentsOpts["linenos"] 312 if lineNumbers != "" { 313 options = append(options, html.WithLineNumbers()) 314 if lineNumbers != "inline" { 315 options = append(options, html.LineNumbersInTable()) 316 } 317 } 318 319 startLineStr := pygmentsOpts["linenostart"] 320 var startLine = 1 321 if startLineStr != "" { 322 323 line, err := strconv.Atoi(strings.TrimSpace(startLineStr)) 324 if err == nil { 325 startLine = line 326 options = append(options, html.BaseLineNumber(startLine)) 327 } 328 } 329 330 hlLines := pygmentsOpts["hl_lines"] 331 332 if hlLines != "" { 333 ranges, err := hlLinesToRanges(startLine, hlLines) 334 335 if err == nil { 336 options = append(options, html.HighlightLines(ranges)) 337 } 338 } 339 340 return html.New(options...), nil 341 } 342 343 func (cs *ContentSpec) parsePygmentsOpts(in string) (map[string]string, error) { 344 opts, err := parseOptions(cs.defatultPygmentsOpts, in) 345 if err != nil { 346 return nil, err 347 } 348 return opts, nil 349 350 } 351 352 func (cs *ContentSpec) createPygmentsOptionsString(in string) (string, error) { 353 opts, err := cs.parsePygmentsOpts(in) 354 if err != nil { 355 return "", err 356 } 357 return createOptionsString(opts), nil 358 } 359 360 // startLine compansates for https://github.com/alecthomas/chroma/issues/30 361 func hlLinesToRanges(startLine int, s string) ([][2]int, error) { 362 var ranges [][2]int 363 s = strings.TrimSpace(s) 364 365 if s == "" { 366 return ranges, nil 367 } 368 369 // Variants: 370 // 1 2 3 4 371 // 1-2 3-4 372 // 1-2 3 373 // 1 3-4 374 // 1 3-4 375 fields := strings.Split(s, " ") 376 for _, field := range fields { 377 field = strings.TrimSpace(field) 378 if field == "" { 379 continue 380 } 381 numbers := strings.Split(field, "-") 382 var r [2]int 383 first, err := strconv.Atoi(numbers[0]) 384 if err != nil { 385 return ranges, err 386 } 387 first = first + startLine - 1 388 r[0] = first 389 if len(numbers) > 1 { 390 second, err := strconv.Atoi(numbers[1]) 391 if err != nil { 392 return ranges, err 393 } 394 second = second + startLine - 1 395 r[1] = second 396 } else { 397 r[1] = first 398 } 399 400 ranges = append(ranges, r) 401 } 402 return ranges, nil 403 404 }