summaryrefslogtreecommitdiff
path: root/vendor/github.com/alecthomas/chroma/v2/formatters
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/alecthomas/chroma/v2/formatters')
-rw-r--r--vendor/github.com/alecthomas/chroma/v2/formatters/html/html.go563
1 files changed, 563 insertions, 0 deletions
diff --git a/vendor/github.com/alecthomas/chroma/v2/formatters/html/html.go b/vendor/github.com/alecthomas/chroma/v2/formatters/html/html.go
new file mode 100644
index 0000000..0a45d87
--- /dev/null
+++ b/vendor/github.com/alecthomas/chroma/v2/formatters/html/html.go
@@ -0,0 +1,563 @@
1package html
2
3import (
4 "fmt"
5 "html"
6 "io"
7 "sort"
8 "strings"
9
10 "github.com/alecthomas/chroma/v2"
11)
12
13// Option sets an option of the HTML formatter.
14type Option func(f *Formatter)
15
16// Standalone configures the HTML formatter for generating a standalone HTML document.
17func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
18
19// ClassPrefix sets the CSS class prefix.
20func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
21
22// WithClasses emits HTML using CSS classes, rather than inline styles.
23func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
24
25// WithAllClasses disables an optimisation that omits redundant CSS classes.
26func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
27
28// WithCustomCSS sets user's custom CSS styles.
29func WithCustomCSS(css map[chroma.TokenType]string) Option {
30 return func(f *Formatter) {
31 f.customCSS = css
32 }
33}
34
35// TabWidth sets the number of characters for a tab. Defaults to 8.
36func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
37
38// PreventSurroundingPre prevents the surrounding pre tags around the generated code.
39func PreventSurroundingPre(b bool) Option {
40 return func(f *Formatter) {
41 f.preventSurroundingPre = b
42
43 if b {
44 f.preWrapper = nopPreWrapper
45 } else {
46 f.preWrapper = defaultPreWrapper
47 }
48 }
49}
50
51// InlineCode creates inline code wrapped in a code tag.
52func InlineCode(b bool) Option {
53 return func(f *Formatter) {
54 f.inlineCode = b
55 f.preWrapper = preWrapper{
56 start: func(code bool, styleAttr string) string {
57 if code {
58 return fmt.Sprintf(`<code%s>`, styleAttr)
59 }
60
61 return ``
62 },
63 end: func(code bool) string {
64 if code {
65 return `</code>`
66 }
67
68 return ``
69 },
70 }
71 }
72}
73
74// WithPreWrapper allows control of the surrounding pre tags.
75func WithPreWrapper(wrapper PreWrapper) Option {
76 return func(f *Formatter) {
77 f.preWrapper = wrapper
78 }
79}
80
81// WrapLongLines wraps long lines.
82func WrapLongLines(b bool) Option {
83 return func(f *Formatter) {
84 f.wrapLongLines = b
85 }
86}
87
88// WithLineNumbers formats output with line numbers.
89func WithLineNumbers(b bool) Option {
90 return func(f *Formatter) {
91 f.lineNumbers = b
92 }
93}
94
95// LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers
96// and code in table td's, which make them copy-and-paste friendly.
97func LineNumbersInTable(b bool) Option {
98 return func(f *Formatter) {
99 f.lineNumbersInTable = b
100 }
101}
102
103// LinkableLineNumbers decorates the line numbers HTML elements with an "id"
104// attribute so they can be linked.
105func LinkableLineNumbers(b bool, prefix string) Option {
106 return func(f *Formatter) {
107 f.linkableLineNumbers = b
108 f.lineNumbersIDPrefix = prefix
109 }
110}
111
112// HighlightLines higlights the given line ranges with the Highlight style.
113//
114// A range is the beginning and ending of a range as 1-based line numbers, inclusive.
115func HighlightLines(ranges [][2]int) Option {
116 return func(f *Formatter) {
117 f.highlightRanges = ranges
118 sort.Sort(f.highlightRanges)
119 }
120}
121
122// BaseLineNumber sets the initial number to start line numbering at. Defaults to 1.
123func BaseLineNumber(n int) Option {
124 return func(f *Formatter) {
125 f.baseLineNumber = n
126 }
127}
128
129// New HTML formatter.
130func New(options ...Option) *Formatter {
131 f := &Formatter{
132 baseLineNumber: 1,
133 preWrapper: defaultPreWrapper,
134 }
135 for _, option := range options {
136 option(f)
137 }
138 return f
139}
140
141// PreWrapper defines the operations supported in WithPreWrapper.
142type PreWrapper interface {
143 // Start is called to write a start <pre> element.
144 // The code flag tells whether this block surrounds
145 // highlighted code. This will be false when surrounding
146 // line numbers.
147 Start(code bool, styleAttr string) string
148
149 // End is called to write the end </pre> element.
150 End(code bool) string
151}
152
153type preWrapper struct {
154 start func(code bool, styleAttr string) string
155 end func(code bool) string
156}
157
158func (p preWrapper) Start(code bool, styleAttr string) string {
159 return p.start(code, styleAttr)
160}
161
162func (p preWrapper) End(code bool) string {
163 return p.end(code)
164}
165
166var (
167 nopPreWrapper = preWrapper{
168 start: func(code bool, styleAttr string) string { return "" },
169 end: func(code bool) string { return "" },
170 }
171 defaultPreWrapper = preWrapper{
172 start: func(code bool, styleAttr string) string {
173 if code {
174 return fmt.Sprintf(`<pre tabindex="0"%s><code>`, styleAttr)
175 }
176
177 return fmt.Sprintf(`<pre tabindex="0"%s>`, styleAttr)
178 },
179 end: func(code bool) string {
180 if code {
181 return `</code></pre>`
182 }
183
184 return `</pre>`
185 },
186 }
187)
188
189// Formatter that generates HTML.
190type Formatter struct {
191 standalone bool
192 prefix string
193 Classes bool // Exported field to detect when classes are being used
194 allClasses bool
195 customCSS map[chroma.TokenType]string
196 preWrapper PreWrapper
197 inlineCode bool
198 preventSurroundingPre bool
199 tabWidth int
200 wrapLongLines bool
201 lineNumbers bool
202 lineNumbersInTable bool
203 linkableLineNumbers bool
204 lineNumbersIDPrefix string
205 highlightRanges highlightRanges
206 baseLineNumber int
207}
208
209type highlightRanges [][2]int
210
211func (h highlightRanges) Len() int { return len(h) }
212func (h highlightRanges) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
213func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
214
215func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
216 return f.writeHTML(w, style, iterator.Tokens())
217}
218
219// We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked).
220//
221// OTOH we need to be super careful about correct escaping...
222func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
223 css := f.styleToCSS(style)
224 if !f.Classes {
225 for t, style := range css {
226 css[t] = compressStyle(style)
227 }
228 }
229 if f.standalone {
230 fmt.Fprint(w, "<html>\n")
231 if f.Classes {
232 fmt.Fprint(w, "<style type=\"text/css\">\n")
233 err = f.WriteCSS(w, style)
234 if err != nil {
235 return err
236 }
237 fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
238 fmt.Fprint(w, "</style>")
239 }
240 fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
241 }
242
243 wrapInTable := f.lineNumbers && f.lineNumbersInTable
244
245 lines := chroma.SplitTokensIntoLines(tokens)
246 lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
247 highlightIndex := 0
248
249 if wrapInTable {
250 // List line numbers in its own <td>
251 fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
252 fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
253 fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
254 fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
255 for index := range lines {
256 line := f.baseLineNumber + index
257 highlight, next := f.shouldHighlight(highlightIndex, line)
258 if next {
259 highlightIndex++
260 }
261 if highlight {
262 fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
263 }
264
265 fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
266
267 if highlight {
268 fmt.Fprintf(w, "</span>")
269 }
270 }
271 fmt.Fprint(w, f.preWrapper.End(false))
272 fmt.Fprint(w, "</td>\n")
273 fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
274 }
275
276 fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
277
278 highlightIndex = 0
279 for index, tokens := range lines {
280 // 1-based line number.
281 line := f.baseLineNumber + index
282 highlight, next := f.shouldHighlight(highlightIndex, line)
283 if next {
284 highlightIndex++
285 }
286
287 if !(f.preventSurroundingPre || f.inlineCode) {
288 // Start of Line
289 fmt.Fprint(w, `<span`)
290
291 if highlight {
292 // Line + LineHighlight
293 if f.Classes {
294 fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
295 } else {
296 fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
297 }
298 fmt.Fprint(w, `>`)
299 } else {
300 fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
301 }
302
303 // Line number
304 if f.lineNumbers && !wrapInTable {
305 fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
306 }
307
308 fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
309 }
310
311 for _, token := range tokens {
312 html := html.EscapeString(token.String())
313 attr := f.styleAttr(css, token.Type)
314 if attr != "" {
315 html = fmt.Sprintf("<span%s>%s</span>", attr, html)
316 }
317 fmt.Fprint(w, html)
318 }
319
320 if !(f.preventSurroundingPre || f.inlineCode) {
321 fmt.Fprint(w, `</span>`) // End of CodeLine
322
323 fmt.Fprint(w, `</span>`) // End of Line
324 }
325 }
326 fmt.Fprintf(w, f.preWrapper.End(true))
327
328 if wrapInTable {
329 fmt.Fprint(w, "</td></tr></table>\n")
330 fmt.Fprint(w, "</div>\n")
331 }
332
333 if f.standalone {
334 fmt.Fprint(w, "\n</body>\n")
335 fmt.Fprint(w, "</html>\n")
336 }
337
338 return nil
339}
340
341func (f *Formatter) lineIDAttribute(line int) string {
342 if !f.linkableLineNumbers {
343 return ""
344 }
345 return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
346}
347
348func (f *Formatter) lineTitleWithLinkIfNeeded(lineDigits, line int) string {
349 title := fmt.Sprintf("%*d", lineDigits, line)
350 if !f.linkableLineNumbers {
351 return title
352 }
353 return fmt.Sprintf("<a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#%s\">%s</a>", f.lineID(line), title)
354}
355
356func (f *Formatter) lineID(line int) string {
357 return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
358}
359
360func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
361 next := false
362 for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
363 highlightIndex++
364 next = true
365 }
366 if highlightIndex < len(f.highlightRanges) {
367 hrange := f.highlightRanges[highlightIndex]
368 if line >= hrange[0] && line <= hrange[1] {
369 return true, next
370 }
371 }
372 return false, next
373}
374
375func (f *Formatter) class(t chroma.TokenType) string {
376 for t != 0 {
377 if cls, ok := chroma.StandardTypes[t]; ok {
378 if cls != "" {
379 return f.prefix + cls
380 }
381 return ""
382 }
383 t = t.Parent()
384 }
385 if cls := chroma.StandardTypes[t]; cls != "" {
386 return f.prefix + cls
387 }
388 return ""
389}
390
391func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
392 if f.Classes {
393 cls := f.class(tt)
394 if cls == "" {
395 return ""
396 }
397 return fmt.Sprintf(` class="%s"`, cls)
398 }
399 if _, ok := styles[tt]; !ok {
400 tt = tt.SubCategory()
401 if _, ok := styles[tt]; !ok {
402 tt = tt.Category()
403 if _, ok := styles[tt]; !ok {
404 return ""
405 }
406 }
407 }
408 css := []string{styles[tt]}
409 css = append(css, extraCSS...)
410 return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
411}
412
413func (f *Formatter) tabWidthStyle() string {
414 if f.tabWidth != 0 && f.tabWidth != 8 {
415 return fmt.Sprintf("-moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d;", f.tabWidth)
416 }
417 return ""
418}
419
420// WriteCSS writes CSS style definitions (without any surrounding HTML).
421func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
422 css := f.styleToCSS(style)
423 // Special-case background as it is mapped to the outer ".chroma" class.
424 if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
425 return err
426 }
427 // Special-case PreWrapper as it is the ".chroma" class.
428 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
429 return err
430 }
431 // Special-case code column of table to expand width.
432 if f.lineNumbers && f.lineNumbersInTable {
433 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
434 chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
435 return err
436 }
437 }
438 // Special-case line number highlighting when targeted.
439 if f.lineNumbers || f.lineNumbersInTable {
440 targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
441 for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
442 fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
443 }
444 }
445 tts := []int{}
446 for tt := range css {
447 tts = append(tts, int(tt))
448 }
449 sort.Ints(tts)
450 for _, ti := range tts {
451 tt := chroma.TokenType(ti)
452 switch tt {
453 case chroma.Background, chroma.PreWrapper:
454 continue
455 }
456 class := f.class(tt)
457 if class == "" {
458 continue
459 }
460 styles := css[tt]
461 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
462 return err
463 }
464 }
465 return nil
466}
467
468func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
469 classes := map[chroma.TokenType]string{}
470 bg := style.Get(chroma.Background)
471 // Convert the style.
472 for t := range chroma.StandardTypes {
473 entry := style.Get(t)
474 if t != chroma.Background {
475 entry = entry.Sub(bg)
476 }
477
478 // Inherit from custom CSS provided by user
479 tokenCategory := t.Category()
480 tokenSubCategory := t.SubCategory()
481 if t != tokenCategory {
482 if css, ok := f.customCSS[tokenCategory]; ok {
483 classes[t] = css
484 }
485 }
486 if tokenCategory != tokenSubCategory {
487 if css, ok := f.customCSS[tokenSubCategory]; ok {
488 classes[t] += css
489 }
490 }
491 // Add custom CSS provided by user
492 if css, ok := f.customCSS[t]; ok {
493 classes[t] += css
494 }
495
496 if !f.allClasses && entry.IsZero() && classes[t] == `` {
497 continue
498 }
499
500 styleEntryCSS := StyleEntryToCSS(entry)
501 if styleEntryCSS != `` && classes[t] != `` {
502 styleEntryCSS += `;`
503 }
504 classes[t] = styleEntryCSS + classes[t]
505 }
506 classes[chroma.Background] += `;` + f.tabWidthStyle()
507 classes[chroma.PreWrapper] += classes[chroma.Background]
508 // Make PreWrapper a grid to show highlight style with full width.
509 if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
510 classes[chroma.PreWrapper] += `display: grid;`
511 }
512 // Make PreWrapper wrap long lines.
513 if f.wrapLongLines {
514 classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
515 }
516 lineNumbersStyle := `white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
517 // All rules begin with default rules followed by user provided rules
518 classes[chroma.Line] = `display: flex;` + classes[chroma.Line]
519 classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
520 classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
521 classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
522 classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
523 return classes
524}
525
526// StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
527func StyleEntryToCSS(e chroma.StyleEntry) string {
528 styles := []string{}
529 if e.Colour.IsSet() {
530 styles = append(styles, "color: "+e.Colour.String())
531 }
532 if e.Background.IsSet() {
533 styles = append(styles, "background-color: "+e.Background.String())
534 }
535 if e.Bold == chroma.Yes {
536 styles = append(styles, "font-weight: bold")
537 }
538 if e.Italic == chroma.Yes {
539 styles = append(styles, "font-style: italic")
540 }
541 if e.Underline == chroma.Yes {
542 styles = append(styles, "text-decoration: underline")
543 }
544 return strings.Join(styles, "; ")
545}
546
547// Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
548func compressStyle(s string) string {
549 parts := strings.Split(s, ";")
550 out := []string{}
551 for _, p := range parts {
552 p = strings.Join(strings.Fields(p), " ")
553 p = strings.Replace(p, ": ", ":", 1)
554 if strings.Contains(p, "#") {
555 c := p[len(p)-6:]
556 if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
557 p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
558 }
559 }
560 out = append(out, p)
561 }
562 return strings.Join(out, ";")
563}