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 }