package html
import (
"fmt"
"html"
"io"
"sort"
"strings"
"github.com/alecthomas/chroma/v2"
)
// 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(b bool) Option { return func(f *Formatter) { f.standalone = b } }
// 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(b bool) Option { return func(f *Formatter) { f.Classes = b } }
// WithAllClasses disables an optimisation that omits redundant CSS classes.
func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
// WithCustomCSS sets user's custom CSS styles.
func WithCustomCSS(css map[chroma.TokenType]string) Option {
return func(f *Formatter) {
f.customCSS = css
}
}
// TabWidth sets the number of characters for a tab. Defaults to 8.
func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
// PreventSurroundingPre prevents the surrounding pre tags around the generated code.
func PreventSurroundingPre(b bool) Option {
return func(f *Formatter) {
f.preventSurroundingPre = b
if b {
f.preWrapper = nopPreWrapper
} else {
f.preWrapper = defaultPreWrapper
}
}
}
// InlineCode creates inline code wrapped in a code tag.
func InlineCode(b bool) Option {
return func(f *Formatter) {
f.inlineCode = b
f.preWrapper = preWrapper{
start: func(code bool, styleAttr string) string {
if code {
return fmt.Sprintf(``, styleAttr)
}
return ``
},
end: func(code bool) string {
if code {
return `
`
}
return ``
},
}
}
}
// WithPreWrapper allows control of the surrounding pre tags.
func WithPreWrapper(wrapper PreWrapper) Option {
return func(f *Formatter) {
f.preWrapper = wrapper
}
}
// WrapLongLines wraps long lines.
func WrapLongLines(b bool) Option {
return func(f *Formatter) {
f.wrapLongLines = b
}
}
// WithLineNumbers formats output with line numbers.
func WithLineNumbers(b bool) Option {
return func(f *Formatter) {
f.lineNumbers = b
}
}
// 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(b bool) Option {
return func(f *Formatter) {
f.lineNumbersInTable = b
}
}
// LinkableLineNumbers decorates the line numbers HTML elements with an "id"
// attribute so they can be linked.
func LinkableLineNumbers(b bool, prefix string) Option {
return func(f *Formatter) {
f.linkableLineNumbers = b
f.lineNumbersIDPrefix = prefix
}
}
// 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,
preWrapper: defaultPreWrapper,
}
for _, option := range options {
option(f)
}
return f
}
// PreWrapper defines the operations supported in WithPreWrapper.
type PreWrapper interface {
// Start is called to write a start
element. // The code flag tells whether this block surrounds // highlighted code. This will be false when surrounding // line numbers. Start(code bool, styleAttr string) string // End is called to write the endelement. End(code bool) string } type preWrapper struct { start func(code bool, styleAttr string) string end func(code bool) string } func (p preWrapper) Start(code bool, styleAttr string) string { return p.start(code, styleAttr) } func (p preWrapper) End(code bool) string { return p.end(code) } var ( nopPreWrapper = preWrapper{ start: func(code bool, styleAttr string) string { return "" }, end: func(code bool) string { return "" }, } defaultPreWrapper = preWrapper{ start: func(code bool, styleAttr string) string { if code { return fmt.Sprintf(`
`, styleAttr)
}
return fmt.Sprintf(``, styleAttr)
},
end: func(code bool) string {
if code {
return `
`
}
return ``
},
}
)
// Formatter that generates HTML.
type Formatter struct {
standalone bool
prefix string
Classes bool // Exported field to detect when classes are being used
allClasses bool
customCSS map[chroma.TokenType]string
preWrapper PreWrapper
inlineCode bool
preventSurroundingPre bool
tabWidth int
wrapLongLines bool
lineNumbers bool
lineNumbersInTable bool
linkableLineNumbers bool
lineNumbersIDPrefix string
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) {
return f.writeHTML(w, style, iterator.Tokens())
}
// 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
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 := chroma.SplitTokensIntoLines(tokens)
lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
highlightIndex := 0
if wrapInTable {
// List line numbers in its own \n", f.styleAttr(css, chroma.LineTableTD)) fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper))) 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, "%s\n", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line)) if highlight { fmt.Fprintf(w, "") } } fmt.Fprint(w, f.preWrapper.End(false)) fmt.Fprint(w, " | \n") fmt.Fprintf(w, "\n", f.styleAttr(css, chroma.LineTableTD, "width:100%")) } fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper))) 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 !(f.preventSurroundingPre || f.inlineCode) { // Start of Line fmt.Fprint(w, ``) } else { fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line)) } // Line number if f.lineNumbers && !wrapInTable { fmt.Fprintf(w, "%s", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line)) } fmt.Fprintf(w, ``, f.styleAttr(css, chroma.CodeLine)) } 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 !(f.preventSurroundingPre || f.inlineCode) { fmt.Fprint(w, ``) // End of CodeLine fmt.Fprint(w, ``) // End of Line } } fmt.Fprintf(w, f.preWrapper.End(true)) if wrapInTable { fmt.Fprint(w, " |