2017-07-19 23:51:16 -07:00
|
|
|
package html
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"io"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/alecthomas/chroma"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Option sets an option of the HTML formatter.
|
2017-09-19 23:04:10 +10:00
|
|
|
type Option func(f *Formatter)
|
2017-07-19 23:51:16 -07:00
|
|
|
|
|
|
|
// Standalone configures the HTML formatter for generating a standalone HTML document.
|
2017-09-19 23:04:10 +10:00
|
|
|
func Standalone() Option { return func(f *Formatter) { f.standalone = true } }
|
2017-07-19 23:51:16 -07:00
|
|
|
|
|
|
|
// ClassPrefix sets the CSS class prefix.
|
2017-09-19 23:04:10 +10:00
|
|
|
func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
|
2017-07-19 23:51:16 -07:00
|
|
|
|
|
|
|
// WithClasses emits HTML using CSS classes, rather than inline styles.
|
2017-09-19 23:04:10 +10:00
|
|
|
func WithClasses() Option { return func(f *Formatter) { f.classes = true } }
|
2017-07-19 23:51:16 -07:00
|
|
|
|
2017-09-19 13:14:29 +10:00
|
|
|
// TabWidth sets the number of characters for a tab. Defaults to 8.
|
2017-09-19 23:04:10 +10:00
|
|
|
func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
|
2017-09-19 13:14:29 +10:00
|
|
|
|
2017-09-19 23:04:10 +10:00
|
|
|
// WithLineNumbers formats output with line numbers.
|
|
|
|
func WithLineNumbers() Option {
|
|
|
|
return func(f *Formatter) {
|
|
|
|
f.lineNumbers = true
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-20 14:15:06 +10:00
|
|
|
// HighlightLines higlights the given line ranges with the Highlight style.
|
2017-09-19 23:04:10 +10:00
|
|
|
//
|
|
|
|
// A range is the beginning and ending of a range as 1-based line numbers, inclusive.
|
2017-09-20 14:15:06 +10:00
|
|
|
func HighlightLines(ranges [][2]int) Option {
|
2017-09-19 23:04:10 +10:00
|
|
|
return func(f *Formatter) {
|
|
|
|
f.highlightRanges = ranges
|
2017-09-20 13:30:46 +10:00
|
|
|
sort.Sort(f.highlightRanges)
|
2017-09-19 23:04:10 +10:00
|
|
|
}
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
|
|
|
|
2017-09-19 23:04:10 +10:00
|
|
|
// New HTML formatter.
|
|
|
|
func New(options ...Option) *Formatter {
|
|
|
|
f := &Formatter{}
|
|
|
|
for _, option := range options {
|
|
|
|
option(f)
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-19 23:04:10 +10:00
|
|
|
return f
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
|
|
|
|
2017-09-19 23:04:10 +10:00
|
|
|
// Formatter that generates HTML.
|
|
|
|
type Formatter struct {
|
|
|
|
standalone bool
|
|
|
|
prefix string
|
|
|
|
classes bool
|
|
|
|
tabWidth int
|
|
|
|
lineNumbers bool
|
2017-09-20 13:30:46 +10:00
|
|
|
highlightRanges highlightRanges
|
2017-09-19 13:14:29 +10:00
|
|
|
}
|
|
|
|
|
2017-09-20 13:30:46 +10:00
|
|
|
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] }
|
|
|
|
|
2017-09-19 23:04:10 +10:00
|
|
|
func (f *Formatter) Format(w io.Writer, style *chroma.Style) (func(*chroma.Token), error) {
|
2017-09-20 13:30:46 +10:00
|
|
|
tokens := []*chroma.Token{}
|
|
|
|
return func(token *chroma.Token) {
|
|
|
|
tokens = append(tokens, token)
|
|
|
|
if token.Type == chroma.EOF {
|
|
|
|
f.writeHTML(w, style, tokens)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []*chroma.Token) error {
|
|
|
|
// 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...
|
|
|
|
css := f.styleToCSS(style)
|
2017-09-19 23:04:10 +10:00
|
|
|
if !f.classes {
|
2017-09-20 13:30:46 +10:00
|
|
|
for t, style := range css {
|
|
|
|
css[t] = compressStyle(style)
|
2017-09-19 23:04:10 +10:00
|
|
|
}
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-19 23:04:10 +10:00
|
|
|
if f.standalone {
|
|
|
|
fmt.Fprint(w, "<html>\n")
|
|
|
|
if f.classes {
|
|
|
|
fmt.Fprint(w, "<style type=\"text/css\">\n")
|
|
|
|
f.WriteCSS(w, style)
|
2017-09-20 13:30:46 +10:00
|
|
|
fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
|
2017-09-19 23:04:10 +10:00
|
|
|
fmt.Fprint(w, "</style>")
|
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
|
|
|
|
fmt.Fprintf(w, "<pre%s>\n", f.styleAttr(css, chroma.Background))
|
|
|
|
lines := splitTokensIntoLines(tokens)
|
|
|
|
lineDigits := len(fmt.Sprintf("%d", len(lines)))
|
2017-09-20 14:15:06 +10:00
|
|
|
highlightIndex := 0
|
2017-09-20 13:30:46 +10:00
|
|
|
for line, tokens := range lines {
|
2017-09-20 14:15:06 +10:00
|
|
|
highlight := false
|
|
|
|
for highlightIndex < len(f.highlightRanges) && line+1 > f.highlightRanges[highlightIndex][1] {
|
|
|
|
highlightIndex++
|
|
|
|
}
|
|
|
|
if highlightIndex < len(f.highlightRanges) {
|
|
|
|
hrange := f.highlightRanges[highlightIndex]
|
|
|
|
if line+1 >= hrange[0] && line+1 <= hrange[1] {
|
|
|
|
highlight = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if highlight {
|
|
|
|
fmt.Fprintf(w, "<span class=\"hl\">")
|
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
if f.lineNumbers {
|
|
|
|
fmt.Fprintf(w, "<span class=\"ln\">%*d</span>", lineDigits, line+1)
|
2017-09-19 10:29:24 +10:00
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
|
|
|
|
for _, token := range tokens {
|
|
|
|
html := html.EscapeString(token.String())
|
|
|
|
attr := f.styleAttr(css, token.Type)
|
|
|
|
if attr != "" {
|
|
|
|
html = fmt.Sprintf("<span%s>%s</span>", attr, html)
|
|
|
|
}
|
2017-09-19 10:29:24 +10:00
|
|
|
fmt.Fprint(w, html)
|
|
|
|
}
|
2017-09-20 14:15:06 +10:00
|
|
|
if highlight {
|
|
|
|
fmt.Fprintf(w, "</span>")
|
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprint(w, "</pre>\n")
|
|
|
|
if f.standalone {
|
|
|
|
fmt.Fprint(w, "</body>\n")
|
|
|
|
fmt.Fprint(w, "</html>\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
|
|
|
|
2017-09-19 23:04:10 +10:00
|
|
|
func (f *Formatter) class(tt chroma.TokenType) string {
|
2017-09-20 13:30:46 +10:00
|
|
|
switch tt {
|
|
|
|
case chroma.Background:
|
2017-09-19 23:04:10 +10:00
|
|
|
return "chroma"
|
2017-09-20 13:30:46 +10:00
|
|
|
case chroma.LineNumbers:
|
|
|
|
return "ln"
|
2017-09-20 14:15:06 +10:00
|
|
|
case chroma.LineHighlight:
|
2017-09-20 13:30:46 +10:00
|
|
|
return "hl"
|
2017-09-19 23:04:10 +10:00
|
|
|
}
|
|
|
|
if tt < 0 {
|
|
|
|
return fmt.Sprintf("%sss%x", f.prefix, -int(tt))
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%ss%x", f.prefix, int(tt))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
|
|
|
|
if _, ok := styles[tt]; !ok {
|
|
|
|
tt = tt.SubCategory()
|
|
|
|
if _, ok := styles[tt]; !ok {
|
|
|
|
tt = tt.Category()
|
|
|
|
if _, ok := styles[tt]; !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if f.classes {
|
|
|
|
return string(fmt.Sprintf(` class="%s"`, f.class(tt)))
|
|
|
|
}
|
|
|
|
return string(fmt.Sprintf(` style="%s"`, styles[tt]))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Formatter) tabWidthStyle() string {
|
|
|
|
if f.tabWidth != 0 && f.tabWidth != 8 {
|
|
|
|
return fmt.Sprintf("; -moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d", f.tabWidth)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2017-09-18 13:59:11 +10:00
|
|
|
// WriteCSS writes CSS style definitions (without any surrounding HTML).
|
2017-09-19 23:04:10 +10:00
|
|
|
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
2017-09-20 13:30:46 +10:00
|
|
|
css := f.styleToCSS(style)
|
|
|
|
// Special-case background as it is mapped to the outer ".chroma" class.
|
|
|
|
if _, err := fmt.Fprintf(w, "/* %s */ .chroma { %s }\n", chroma.Background, css[chroma.Background]); err != nil {
|
2017-09-18 13:59:11 +10:00
|
|
|
return err
|
|
|
|
}
|
2017-07-19 23:51:16 -07:00
|
|
|
tts := []int{}
|
2017-09-20 13:30:46 +10:00
|
|
|
for tt := range css {
|
2017-07-19 23:51:16 -07:00
|
|
|
tts = append(tts, int(tt))
|
|
|
|
}
|
|
|
|
sort.Ints(tts)
|
|
|
|
for _, ti := range tts {
|
|
|
|
tt := chroma.TokenType(ti)
|
2017-09-20 13:30:46 +10:00
|
|
|
if tt == chroma.Background {
|
2017-07-19 23:51:16 -07:00
|
|
|
continue
|
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
styles := css[tt]
|
|
|
|
if _, err := fmt.Fprintf(w, "/* %s */ .chroma .%s { %s }\n", tt, f.class(tt), styles); err != nil {
|
2017-09-18 13:59:11 +10:00
|
|
|
return err
|
|
|
|
}
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-18 13:59:11 +10:00
|
|
|
return nil
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
|
|
|
|
2017-09-20 13:30:46 +10:00
|
|
|
func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
|
2017-07-19 23:51:16 -07:00
|
|
|
bg := style.Get(chroma.Background)
|
|
|
|
classes := map[chroma.TokenType]string{}
|
2017-09-20 13:30:46 +10:00
|
|
|
// Convert the style.
|
2017-07-19 23:51:16 -07:00
|
|
|
for t := range style.Entries {
|
|
|
|
e := style.Entries[t]
|
|
|
|
if t != chroma.Background {
|
|
|
|
e = e.Sub(bg)
|
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
classes[t] = StyleEntryToCSS(e)
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-19 23:04:10 +10:00
|
|
|
classes[chroma.Background] += f.tabWidthStyle()
|
2017-09-20 14:15:06 +10:00
|
|
|
classes[chroma.LineNumbers] += "; margin-right: 0.5em"
|
|
|
|
classes[chroma.LineHighlight] += "; display: block; width: 100%"
|
2017-07-19 23:51:16 -07:00
|
|
|
return classes
|
|
|
|
}
|
|
|
|
|
2017-09-20 13:30:46 +10:00
|
|
|
// StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
|
|
|
|
func StyleEntryToCSS(e *chroma.StyleEntry) string {
|
2017-07-19 23:51:16 -07:00
|
|
|
styles := []string{}
|
|
|
|
if e.Colour.IsSet() {
|
|
|
|
styles = append(styles, "color: "+e.Colour.String())
|
|
|
|
}
|
|
|
|
if e.Background.IsSet() {
|
|
|
|
styles = append(styles, "background-color: "+e.Background.String())
|
|
|
|
}
|
|
|
|
if e.Bold {
|
|
|
|
styles = append(styles, "font-weight: bold")
|
|
|
|
}
|
|
|
|
if e.Italic {
|
|
|
|
styles = append(styles, "font-style: italic")
|
|
|
|
}
|
2017-09-19 23:04:10 +10:00
|
|
|
return strings.Join(styles, "; ")
|
2017-07-19 23:51:16 -07:00
|
|
|
}
|
2017-09-20 13:30:46 +10:00
|
|
|
|
|
|
|
// Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
|
|
|
|
func compressStyle(s string) string {
|
|
|
|
s = strings.Replace(s, " ", "", -1)
|
|
|
|
parts := strings.Split(s, ";")
|
|
|
|
out := []string{}
|
|
|
|
for _, p := range parts {
|
|
|
|
if strings.Contains(p, "#") {
|
|
|
|
c := p[len(p)-6:]
|
|
|
|
if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
|
|
|
|
p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
out = append(out, p)
|
|
|
|
}
|
|
|
|
return strings.Join(out, ";")
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitTokensIntoLines(tokens []*chroma.Token) (out [][]*chroma.Token) {
|
|
|
|
line := []*chroma.Token{}
|
|
|
|
for _, token := range tokens {
|
|
|
|
for strings.Contains(token.Value, "\n") {
|
|
|
|
parts := strings.SplitAfterN(token.Value, "\n", 2)
|
|
|
|
// Token becomes the tail.
|
|
|
|
token.Value = parts[1]
|
|
|
|
|
|
|
|
// Append the head to the line and flush the line.
|
|
|
|
clone := token.Clone()
|
|
|
|
clone.Value = parts[0]
|
|
|
|
line = append(line, clone)
|
|
|
|
out = append(out, line)
|
|
|
|
line = nil
|
|
|
|
}
|
|
|
|
line = append(line, token)
|
|
|
|
}
|
|
|
|
if len(line) > 0 {
|
|
|
|
out = append(out, line)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|