package html import ( "fmt" "html" "io" "sort" "strings" "github.com/alecthomas/chroma" ) // Option sets an option of the HTML formatter. type Option func(f *Formatter) // Standalone configures the HTML formatter for generating a standalone HTML document. func Standalone() Option { return func(f *Formatter) { f.standalone = true } } // ClassPrefix sets the CSS class prefix. func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } } // WithClasses emits HTML using CSS classes, rather than inline styles. func WithClasses() Option { return func(f *Formatter) { f.Classes = true } } // TabWidth sets the number of characters for a tab. Defaults to 8. func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } } // WithLineNumbers formats output with line numbers. func WithLineNumbers() Option { return func(f *Formatter) { f.lineNumbers = true } } // LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers // and code in table td's, which make them copy-and-paste friendly. func LineNumbersInTable() Option { return func(f *Formatter) { f.lineNumbersInTable = true } } // HighlightLines higlights the given line ranges with the Highlight style. // // A range is the beginning and ending of a range as 1-based line numbers, inclusive. func HighlightLines(ranges [][2]int) Option { return func(f *Formatter) { f.highlightRanges = ranges sort.Sort(f.highlightRanges) } } // BaseLineNumber sets the initial number to start line numbering at. Defaults to 1. func BaseLineNumber(n int) Option { return func(f *Formatter) { f.baseLineNumber = n } } // New HTML formatter. func New(options ...Option) *Formatter { f := &Formatter{ baseLineNumber: 1, } for _, option := range options { option(f) } return f } // Formatter that generates HTML. type Formatter struct { standalone bool prefix string Classes bool // Exported field to detect when classes are being used tabWidth int lineNumbers bool lineNumbersInTable bool highlightRanges highlightRanges baseLineNumber int } type highlightRanges [][2]int func (h highlightRanges) Len() int { return len(h) } func (h highlightRanges) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] } func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) { defer func() { if perr := recover(); perr != nil { err = perr.(error) } }() return f.writeHTML(w, style, iterator.Tokens()) } func brightenOrDarken(colour chroma.Colour, factor float64) chroma.Colour { if colour.Brightness() < 0.5 { return colour.Brighten(factor) } return colour.Brighten(-factor) } // Ensure that style entries exist for highlighting, etc. func (f *Formatter) restyle(style *chroma.Style) (*chroma.Style, error) { builder := style.Builder() bg := builder.Get(chroma.Background) // If we don't have a line highlight colour, make one that is 10% brighter/darker than the background. if !style.Has(chroma.LineHighlight) { highlight := chroma.StyleEntry{Background: bg.Background} highlight.Background = brightenOrDarken(highlight.Background, 0.1) builder.AddEntry(chroma.LineHighlight, highlight) } // If we don't have line numbers, use the text colour but 20% brighter/darker if !style.Has(chroma.LineNumbers) { text := chroma.StyleEntry{Colour: bg.Colour} text.Colour = brightenOrDarken(text.Colour, 0.5) builder.AddEntry(chroma.LineNumbers, text) builder.AddEntry(chroma.LineNumbersTable, text) } return builder.Build() } // We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked). // // OTOH we need to be super careful about correct escaping... func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []*chroma.Token) (err error) { // nolint: gocyclo style, err = f.restyle(style) if err != nil { return err } css := f.styleToCSS(style) if !f.Classes { for t, style := range css { css[t] = compressStyle(style) } } if f.standalone { fmt.Fprint(w, "\n") if f.Classes { fmt.Fprint(w, "") } fmt.Fprintf(w, "
\n", f.styleAttr(css, chroma.Background)) } wrapInTable := f.lineNumbers && f.lineNumbersInTable lines := splitTokensIntoLines(tokens) lineDigits := len(fmt.Sprintf("%d", len(lines))) highlightIndex := 0 if wrapInTable { // List line numbers in its own\n", f.styleAttr(css, chroma.LineTableTD))
fmt.Fprintf(w, "", f.styleAttr(css, chroma.Background))
for index := range lines {
line := f.baseLineNumber + index
highlight, next := f.shouldHighlight(highlightIndex, line)
if next {
highlightIndex++
}
if highlight {
fmt.Fprintf(w, "", f.styleAttr(css, chroma.LineHighlight))
}
fmt.Fprintf(w, "%*d\n", f.styleAttr(css, chroma.LineNumbersTable), lineDigits, line)
if highlight {
fmt.Fprintf(w, "")
}
}
fmt.Fprint(w, " | \n")
fmt.Fprintf(w, "\n", f.styleAttr(css, chroma.LineTableTD))
}
fmt.Fprintf(w, "", f.styleAttr(css, chroma.Background))
highlightIndex = 0
for index, tokens := range lines {
// 1-based line number.
line := f.baseLineNumber + index
highlight, next := f.shouldHighlight(highlightIndex, line)
if next {
highlightIndex++
}
if highlight {
fmt.Fprintf(w, "", f.styleAttr(css, chroma.LineHighlight))
}
if f.lineNumbers && !wrapInTable {
fmt.Fprintf(w, "%*d", f.styleAttr(css, chroma.LineNumbers), lineDigits, line)
}
for _, token := range tokens {
html := html.EscapeString(token.String())
attr := f.styleAttr(css, token.Type)
if attr != "" {
html = fmt.Sprintf("%s", attr, html)
}
fmt.Fprint(w, html)
}
if highlight {
fmt.Fprintf(w, "")
}
}
fmt.Fprint(w, " ")
if wrapInTable {
fmt.Fprint(w, " |