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(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 } } // 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) { if b { f.preWrapper = nopPreWrapper } else { f.preWrapper = defaultPreWrapper } } } // WithPreWrapper allows control of the surrounding pre tags. func WithPreWrapper(wrapper PreWrapper) Option { return func(f *Formatter) { f.preWrapper = wrapper } } // 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 { return fmt.Sprintf("
", styleAttr) }, end: func(code bool) string { 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 preWrapper PreWrapper tabWidth int 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.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, "%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.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, "%s", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(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.Fprintf(w, f.preWrapper.End(true)) if wrapInTable { fmt.Fprint(w, " |