config.go (6741B)
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 provides code highlighting. 15 package highlight 16 17 import ( 18 "fmt" 19 "strconv" 20 "strings" 21 22 "github.com/alecthomas/chroma/v2/formatters/html" 23 "github.com/spf13/cast" 24 25 "github.com/gohugoio/hugo/config" 26 "github.com/gohugoio/hugo/markup/converter/hooks" 27 28 "github.com/mitchellh/mapstructure" 29 ) 30 31 const ( 32 lineanchorsKey = "lineanchors" 33 lineNosKey = "linenos" 34 hlLinesKey = "hl_lines" 35 linosStartKey = "linenostart" 36 noHlKey = "nohl" 37 ) 38 39 var DefaultConfig = Config{ 40 // The highlighter style to use. 41 // See https://xyproto.github.io/splash/docs/all.html 42 Style: "monokai", 43 LineNoStart: 1, 44 CodeFences: true, 45 NoClasses: true, 46 LineNumbersInTable: true, 47 TabWidth: 4, 48 } 49 50 type Config struct { 51 Style string 52 53 CodeFences bool 54 55 // Use inline CSS styles. 56 NoClasses bool 57 58 // No highlighting. 59 NoHl bool 60 61 // When set, line numbers will be printed. 62 LineNos bool 63 LineNumbersInTable bool 64 65 // When set, add links to line numbers 66 AnchorLineNos bool 67 LineAnchors string 68 69 // Start the line numbers from this value (default is 1). 70 LineNoStart int 71 72 // A space separated list of line numbers, e.g. “3-8 10-20”. 73 Hl_Lines string 74 75 // If set, the markup will not be wrapped in any container. 76 Hl_inline bool 77 78 // A parsed and ready to use list of line ranges. 79 HL_lines_parsed [][2]int `json:"-"` 80 81 // TabWidth sets the number of characters for a tab. Defaults to 4. 82 TabWidth int 83 84 GuessSyntax bool 85 } 86 87 func (cfg Config) ToHTMLOptions() []html.Option { 88 var lineAnchors string 89 if cfg.LineAnchors != "" { 90 lineAnchors = cfg.LineAnchors + "-" 91 } 92 options := []html.Option{ 93 html.TabWidth(cfg.TabWidth), 94 html.WithLineNumbers(cfg.LineNos), 95 html.BaseLineNumber(cfg.LineNoStart), 96 html.LineNumbersInTable(cfg.LineNumbersInTable), 97 html.WithClasses(!cfg.NoClasses), 98 html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors), 99 html.InlineCode(cfg.Hl_inline), 100 } 101 102 if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil { 103 var ranges [][2]int 104 if cfg.HL_lines_parsed != nil { 105 ranges = cfg.HL_lines_parsed 106 } else { 107 var err error 108 ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines) 109 if err != nil { 110 ranges = nil 111 } 112 } 113 114 if ranges != nil { 115 options = append(options, html.HighlightLines(ranges)) 116 } 117 } 118 119 return options 120 } 121 122 func applyOptions(opts any, cfg *Config) error { 123 if opts == nil { 124 return nil 125 } 126 switch vv := opts.(type) { 127 case map[string]any: 128 return applyOptionsFromMap(vv, cfg) 129 default: 130 s, err := cast.ToStringE(opts) 131 if err != nil { 132 return err 133 } 134 return applyOptionsFromString(s, cfg) 135 } 136 } 137 138 func applyOptionsFromString(opts string, cfg *Config) error { 139 optsm, err := parseHightlightOptions(opts) 140 if err != nil { 141 return err 142 } 143 return mapstructure.WeakDecode(optsm, cfg) 144 } 145 146 func applyOptionsFromMap(optsm map[string]any, cfg *Config) error { 147 normalizeHighlightOptions(optsm) 148 return mapstructure.WeakDecode(optsm, cfg) 149 } 150 151 func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error { 152 if cfg.LineAnchors == "" { 153 const lineAnchorPrefix = "hl-" 154 // Set it to the ordinal with a prefix. 155 cfg.LineAnchors = fmt.Sprintf("%s%d", lineAnchorPrefix, ctx.Ordinal()) 156 } 157 158 return nil 159 } 160 161 // ApplyLegacyConfig applies legacy config from back when we had 162 // Pygments. 163 func ApplyLegacyConfig(cfg config.Provider, conf *Config) error { 164 if conf.Style == DefaultConfig.Style { 165 if s := cfg.GetString("pygmentsStyle"); s != "" { 166 conf.Style = s 167 } 168 } 169 170 if conf.NoClasses == DefaultConfig.NoClasses && cfg.IsSet("pygmentsUseClasses") { 171 conf.NoClasses = !cfg.GetBool("pygmentsUseClasses") 172 } 173 174 if conf.CodeFences == DefaultConfig.CodeFences && cfg.IsSet("pygmentsCodeFences") { 175 conf.CodeFences = cfg.GetBool("pygmentsCodeFences") 176 } 177 178 if conf.GuessSyntax == DefaultConfig.GuessSyntax && cfg.IsSet("pygmentsCodefencesGuessSyntax") { 179 conf.GuessSyntax = cfg.GetBool("pygmentsCodefencesGuessSyntax") 180 } 181 182 if cfg.IsSet("pygmentsOptions") { 183 if err := applyOptionsFromString(cfg.GetString("pygmentsOptions"), conf); err != nil { 184 return err 185 } 186 } 187 188 return nil 189 } 190 191 func parseHightlightOptions(in string) (map[string]any, error) { 192 in = strings.Trim(in, " ") 193 opts := make(map[string]any) 194 195 if in == "" { 196 return opts, nil 197 } 198 199 for _, v := range strings.Split(in, ",") { 200 keyVal := strings.Split(v, "=") 201 key := strings.ToLower(strings.Trim(keyVal[0], " ")) 202 if len(keyVal) != 2 { 203 return opts, fmt.Errorf("invalid Highlight option: %s", key) 204 } 205 opts[key] = keyVal[1] 206 207 } 208 209 normalizeHighlightOptions(opts) 210 211 return opts, nil 212 } 213 214 func normalizeHighlightOptions(m map[string]any) { 215 if m == nil { 216 return 217 } 218 219 baseLineNumber := 1 220 if v, ok := m[linosStartKey]; ok { 221 baseLineNumber = cast.ToInt(v) 222 } 223 224 for k, v := range m { 225 switch k { 226 case noHlKey: 227 m[noHlKey] = cast.ToBool(v) 228 case lineNosKey: 229 if v == "table" || v == "inline" { 230 m["lineNumbersInTable"] = v == "table" 231 } 232 if vs, ok := v.(string); ok { 233 m[k] = vs != "false" 234 } 235 236 case hlLinesKey: 237 if hlRanges, ok := v.([][2]int); ok { 238 for i := range hlRanges { 239 hlRanges[i][0] += baseLineNumber 240 hlRanges[i][1] += baseLineNumber 241 } 242 delete(m, k) 243 m[k+"_parsed"] = hlRanges 244 } 245 } 246 } 247 } 248 249 // startLine compensates for https://github.com/alecthomas/chroma/issues/30 250 func hlLinesToRanges(startLine int, s string) ([][2]int, error) { 251 var ranges [][2]int 252 s = strings.TrimSpace(s) 253 254 if s == "" { 255 return ranges, nil 256 } 257 258 // Variants: 259 // 1 2 3 4 260 // 1-2 3-4 261 // 1-2 3 262 // 1 3-4 263 // 1 3-4 264 fields := strings.Split(s, " ") 265 for _, field := range fields { 266 field = strings.TrimSpace(field) 267 if field == "" { 268 continue 269 } 270 numbers := strings.Split(field, "-") 271 var r [2]int 272 first, err := strconv.Atoi(numbers[0]) 273 if err != nil { 274 return ranges, err 275 } 276 first = first + startLine - 1 277 r[0] = first 278 if len(numbers) > 1 { 279 second, err := strconv.Atoi(numbers[1]) 280 if err != nil { 281 return ranges, err 282 } 283 second = second + startLine - 1 284 r[1] = second 285 } else { 286 r[1] = first 287 } 288 289 ranges = append(ranges, r) 290 } 291 return ranges, nil 292 }