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