1 package html
2
3 import (
4 "fmt"
5 "html"
6 "io"
7 "sort"
8 "strings"
9
10 "github.com/alecthomas/chroma"
11 )
12
13
14 type Option func(f *Formatter)
15
16
17 func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
18
19
20 func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
21
22
23 func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
24
25
26 func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
27
28
29 func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
30
31
32 func PreventSurroundingPre(b bool) Option {
33 return func(f *Formatter) {
34 if b {
35 f.preWrapper = nopPreWrapper
36 } else {
37 f.preWrapper = defaultPreWrapper
38 }
39 }
40 }
41
42
43 func WithPreWrapper(wrapper PreWrapper) Option {
44 return func(f *Formatter) {
45 f.preWrapper = wrapper
46 }
47 }
48
49
50 func WrapLongLines(b bool) Option {
51 return func(f *Formatter) {
52 f.wrapLongLines = b
53 }
54 }
55
56
57 func WithLineNumbers(b bool) Option {
58 return func(f *Formatter) {
59 f.lineNumbers = b
60 }
61 }
62
63
64
65 func LineNumbersInTable(b bool) Option {
66 return func(f *Formatter) {
67 f.lineNumbersInTable = b
68 }
69 }
70
71
72
73 func LinkableLineNumbers(b bool, prefix string) Option {
74 return func(f *Formatter) {
75 f.linkableLineNumbers = b
76 f.lineNumbersIDPrefix = prefix
77 }
78 }
79
80
81
82
83 func HighlightLines(ranges [][2]int) Option {
84 return func(f *Formatter) {
85 f.highlightRanges = ranges
86 sort.Sort(f.highlightRanges)
87 }
88 }
89
90
91 func BaseLineNumber(n int) Option {
92 return func(f *Formatter) {
93 f.baseLineNumber = n
94 }
95 }
96
97
98 func New(options ...Option) *Formatter {
99 f := &Formatter{
100 baseLineNumber: 1,
101 preWrapper: defaultPreWrapper,
102 }
103 for _, option := range options {
104 option(f)
105 }
106 return f
107 }
108
109
110 type PreWrapper interface {
111
112
113
114
115 Start(code bool, styleAttr string) string
116
117
118 End(code bool) string
119 }
120
121 type preWrapper struct {
122 start func(code bool, styleAttr string) string
123 end func(code bool) string
124 }
125
126 func (p preWrapper) Start(code bool, styleAttr string) string {
127 return p.start(code, styleAttr)
128 }
129
130 func (p preWrapper) End(code bool) string {
131 return p.end(code)
132 }
133
134 var (
135 nopPreWrapper = preWrapper{
136 start: func(code bool, styleAttr string) string { return "" },
137 end: func(code bool) string { return "" },
138 }
139 defaultPreWrapper = preWrapper{
140 start: func(code bool, styleAttr string) string {
141 if code {
142 return fmt.Sprintf(`<pre tabindex="0"%s><code>`, styleAttr)
143 }
144
145 return fmt.Sprintf(`<pre tabindex="0"%s>`, styleAttr)
146 },
147 end: func(code bool) string {
148 if code {
149 return `</code></pre>`
150 }
151
152 return `</pre>`
153 },
154 }
155 )
156
157
158 type Formatter struct {
159 standalone bool
160 prefix string
161 Classes bool
162 allClasses bool
163 preWrapper PreWrapper
164 tabWidth int
165 wrapLongLines bool
166 lineNumbers bool
167 lineNumbersInTable bool
168 linkableLineNumbers bool
169 lineNumbersIDPrefix string
170 highlightRanges highlightRanges
171 baseLineNumber int
172 }
173
174 type highlightRanges [][2]int
175
176 func (h highlightRanges) Len() int { return len(h) }
177 func (h highlightRanges) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
178 func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
179
180 func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
181 return f.writeHTML(w, style, iterator.Tokens())
182 }
183
184
185
186
187 func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) {
188 css := f.styleToCSS(style)
189 if !f.Classes {
190 for t, style := range css {
191 css[t] = compressStyle(style)
192 }
193 }
194 if f.standalone {
195 fmt.Fprint(w, "<html>\n")
196 if f.Classes {
197 fmt.Fprint(w, "<style type=\"text/css\">\n")
198 err = f.WriteCSS(w, style)
199 if err != nil {
200 return err
201 }
202 fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
203 fmt.Fprint(w, "</style>")
204 }
205 fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
206 }
207
208 wrapInTable := f.lineNumbers && f.lineNumbersInTable
209
210 lines := chroma.SplitTokensIntoLines(tokens)
211 lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
212 highlightIndex := 0
213
214 if wrapInTable {
215
216 fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
217 fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
218 fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
219 fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
220 for index := range lines {
221 line := f.baseLineNumber + index
222 highlight, next := f.shouldHighlight(highlightIndex, line)
223 if next {
224 highlightIndex++
225 }
226 if highlight {
227 fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
228 }
229
230 fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
231
232 if highlight {
233 fmt.Fprintf(w, "</span>")
234 }
235 }
236 fmt.Fprint(w, f.preWrapper.End(false))
237 fmt.Fprint(w, "</td>\n")
238 fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
239 }
240
241 fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
242
243 highlightIndex = 0
244 for index, tokens := range lines {
245
246 line := f.baseLineNumber + index
247 highlight, next := f.shouldHighlight(highlightIndex, line)
248 if next {
249 highlightIndex++
250 }
251
252
253 fmt.Fprint(w, `<span`)
254 if highlight {
255
256 if f.Classes {
257 fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
258 } else {
259 fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
260 }
261 fmt.Fprint(w, `>`)
262 } else {
263 fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
264 }
265
266
267 if f.lineNumbers && !wrapInTable {
268 fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
269 }
270
271 fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
272
273 for _, token := range tokens {
274 html := html.EscapeString(token.String())
275 attr := f.styleAttr(css, token.Type)
276 if attr != "" {
277 html = fmt.Sprintf("<span%s>%s</span>", attr, html)
278 }
279 fmt.Fprint(w, html)
280 }
281
282 fmt.Fprint(w, `</span>`)
283
284 fmt.Fprint(w, `</span>`)
285 }
286
287 fmt.Fprintf(w, f.preWrapper.End(true))
288
289 if wrapInTable {
290 fmt.Fprint(w, "</td></tr></table>\n")
291 fmt.Fprint(w, "</div>\n")
292 }
293
294 if f.standalone {
295 fmt.Fprint(w, "\n</body>\n")
296 fmt.Fprint(w, "</html>\n")
297 }
298
299 return nil
300 }
301
302 func (f *Formatter) lineIDAttribute(line int) string {
303 if !f.linkableLineNumbers {
304 return ""
305 }
306 return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
307 }
308
309 func (f *Formatter) lineTitleWithLinkIfNeeded(lineDigits, line int) string {
310 title := fmt.Sprintf("%*d", lineDigits, line)
311 if !f.linkableLineNumbers {
312 return title
313 }
314 return fmt.Sprintf("<a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#%s\">%s</a>", f.lineID(line), title)
315 }
316
317 func (f *Formatter) lineID(line int) string {
318 return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
319 }
320
321 func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
322 next := false
323 for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
324 highlightIndex++
325 next = true
326 }
327 if highlightIndex < len(f.highlightRanges) {
328 hrange := f.highlightRanges[highlightIndex]
329 if line >= hrange[0] && line <= hrange[1] {
330 return true, next
331 }
332 }
333 return false, next
334 }
335
336 func (f *Formatter) class(t chroma.TokenType) string {
337 for t != 0 {
338 if cls, ok := chroma.StandardTypes[t]; ok {
339 if cls != "" {
340 return f.prefix + cls
341 }
342 return ""
343 }
344 t = t.Parent()
345 }
346 if cls := chroma.StandardTypes[t]; cls != "" {
347 return f.prefix + cls
348 }
349 return ""
350 }
351
352 func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
353 if f.Classes {
354 cls := f.class(tt)
355 if cls == "" {
356 return ""
357 }
358 return fmt.Sprintf(` class="%s"`, cls)
359 }
360 if _, ok := styles[tt]; !ok {
361 tt = tt.SubCategory()
362 if _, ok := styles[tt]; !ok {
363 tt = tt.Category()
364 if _, ok := styles[tt]; !ok {
365 return ""
366 }
367 }
368 }
369 css := []string{styles[tt]}
370 css = append(css, extraCSS...)
371 return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
372 }
373
374 func (f *Formatter) tabWidthStyle() string {
375 if f.tabWidth != 0 && f.tabWidth != 8 {
376 return fmt.Sprintf("; -moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d", f.tabWidth)
377 }
378 return ""
379 }
380
381
382 func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
383 css := f.styleToCSS(style)
384
385 if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
386 return err
387 }
388
389 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
390 return err
391 }
392
393 if f.lineNumbers && f.lineNumbersInTable {
394 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
395 chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
396 return err
397 }
398 }
399
400 if f.lineNumbers || f.lineNumbersInTable {
401 targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
402 for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
403 fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
404 }
405 }
406 tts := []int{}
407 for tt := range css {
408 tts = append(tts, int(tt))
409 }
410 sort.Ints(tts)
411 for _, ti := range tts {
412 tt := chroma.TokenType(ti)
413 switch tt {
414 case chroma.Background, chroma.PreWrapper:
415 continue
416 }
417 class := f.class(tt)
418 if class == "" {
419 continue
420 }
421 styles := css[tt]
422 if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
423 return err
424 }
425 }
426 return nil
427 }
428
429 func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
430 classes := map[chroma.TokenType]string{}
431 bg := style.Get(chroma.Background)
432
433 for t := range chroma.StandardTypes {
434 entry := style.Get(t)
435 if t != chroma.Background {
436 entry = entry.Sub(bg)
437 }
438 if !f.allClasses && entry.IsZero() {
439 continue
440 }
441 classes[t] = StyleEntryToCSS(entry)
442 }
443 classes[chroma.Background] += f.tabWidthStyle()
444 classes[chroma.PreWrapper] += classes[chroma.Background] + `;`
445
446 if len(f.highlightRanges) > 0 {
447 classes[chroma.PreWrapper] += `display: grid;`
448 }
449
450 if f.wrapLongLines {
451 classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
452 }
453 lineNumbersStyle := `white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
454
455 classes[chroma.Line] = `display: flex;` + classes[chroma.Line]
456 classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
457 classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
458 classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
459 classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
460 return classes
461 }
462
463
464 func StyleEntryToCSS(e chroma.StyleEntry) string {
465 styles := []string{}
466 if e.Colour.IsSet() {
467 styles = append(styles, "color: "+e.Colour.String())
468 }
469 if e.Background.IsSet() {
470 styles = append(styles, "background-color: "+e.Background.String())
471 }
472 if e.Bold == chroma.Yes {
473 styles = append(styles, "font-weight: bold")
474 }
475 if e.Italic == chroma.Yes {
476 styles = append(styles, "font-style: italic")
477 }
478 if e.Underline == chroma.Yes {
479 styles = append(styles, "text-decoration: underline")
480 }
481 return strings.Join(styles, "; ")
482 }
483
484
485 func compressStyle(s string) string {
486 parts := strings.Split(s, ";")
487 out := []string{}
488 for _, p := range parts {
489 p = strings.Join(strings.Fields(p), " ")
490 p = strings.Replace(p, ": ", ":", 1)
491 if strings.Contains(p, "#") {
492 c := p[len(p)-6:]
493 if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
494 p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
495 }
496 }
497 out = append(out, p)
498 }
499 return strings.Join(out, ";")
500 }
501
View as plain text