123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- 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 <pre> 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 end </pre> element.
- 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("<pre%s>", styleAttr)
- },
- end: func(code bool) string {
- return "</pre>"
- },
- }
- )
-
- // 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, "<html>\n")
- if f.Classes {
- fmt.Fprint(w, "<style type=\"text/css\">\n")
- err = f.WriteCSS(w, style)
- if err != nil {
- return err
- }
- fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
- fmt.Fprint(w, "</style>")
- }
- fmt.Fprintf(w, "<body%s>\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 <td>
- fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.Background))
- fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
- fmt.Fprintf(w, "<td%s>\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, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
- }
-
- fmt.Fprintf(w, "<span%s%s>%*d\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), lineDigits, line)
-
- if highlight {
- fmt.Fprintf(w, "</span>")
- }
- }
- fmt.Fprint(w, f.preWrapper.End(false))
- fmt.Fprint(w, "</td>\n")
- fmt.Fprintf(w, "<td%s>\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, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
- }
-
- if f.lineNumbers && !wrapInTable {
- fmt.Fprintf(w, "<span%s%s>%*d</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), lineDigits, line)
- }
-
- 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)
- }
- fmt.Fprint(w, html)
- }
- if highlight {
- fmt.Fprintf(w, "</span>")
- }
- }
-
- fmt.Fprintf(w, f.preWrapper.End(true))
-
- if wrapInTable {
- fmt.Fprint(w, "</td></tr></table>\n")
- fmt.Fprint(w, "</div>\n")
- }
-
- if f.standalone {
- fmt.Fprint(w, "\n</body>\n")
- fmt.Fprint(w, "</html>\n")
- }
-
- return nil
- }
-
- func (f *Formatter) lineIDAttribute(line int) string {
- if !f.linkableLineNumbers {
- return ""
- }
- return fmt.Sprintf(" id=\"%s%d\"", f.lineNumbersIDPrefix, line)
- }
-
- func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
- next := false
- for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
- highlightIndex++
- next = true
- }
- if highlightIndex < len(f.highlightRanges) {
- hrange := f.highlightRanges[highlightIndex]
- if line >= hrange[0] && line <= hrange[1] {
- return true, next
- }
- }
- return false, next
- }
-
- func (f *Formatter) class(t chroma.TokenType) string {
- for t != 0 {
- if cls, ok := chroma.StandardTypes[t]; ok {
- if cls != "" {
- return f.prefix + cls
- }
- return ""
- }
- t = t.Parent()
- }
- if cls := chroma.StandardTypes[t]; cls != "" {
- return f.prefix + cls
- }
- return ""
- }
-
- func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
- if f.Classes {
- cls := f.class(tt)
- if cls == "" {
- return ""
- }
- return fmt.Sprintf(` class="%s"`, cls)
- }
- if _, ok := styles[tt]; !ok {
- tt = tt.SubCategory()
- if _, ok := styles[tt]; !ok {
- tt = tt.Category()
- if _, ok := styles[tt]; !ok {
- return ""
- }
- }
- }
- css := []string{styles[tt]}
- css = append(css, extraCSS...)
- return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
- }
-
- 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 ""
- }
-
- // WriteCSS writes CSS style definitions (without any surrounding HTML).
- func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
- css := f.styleToCSS(style)
- // Special-case background as it is mapped to the outer ".chroma" class.
- if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
- return err
- }
- // Special-case code column of table to expand width.
- if f.lineNumbers && f.lineNumbersInTable {
- if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
- chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
- return err
- }
- }
- // Special-case line number highlighting when targeted.
- if f.lineNumbers || f.lineNumbersInTable {
- targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
- for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
- fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
- }
- }
- tts := []int{}
- for tt := range css {
- tts = append(tts, int(tt))
- }
- sort.Ints(tts)
- for _, ti := range tts {
- tt := chroma.TokenType(ti)
- if tt == chroma.Background {
- continue
- }
- class := f.class(tt)
- if class == "" {
- continue
- }
- styles := css[tt]
- if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
- return err
- }
- }
- return nil
- }
-
- func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
- classes := map[chroma.TokenType]string{}
- bg := style.Get(chroma.Background)
- // Convert the style.
- for t := range chroma.StandardTypes {
- entry := style.Get(t)
- if t != chroma.Background {
- entry = entry.Sub(bg)
- }
- if !f.allClasses && entry.IsZero() {
- continue
- }
- classes[t] = StyleEntryToCSS(entry)
- }
- classes[chroma.Background] += f.tabWidthStyle()
- lineNumbersStyle := "margin-right: 0.4em; padding: 0 0.4em 0 0.4em;"
- // All rules begin with default rules followed by user provided rules
- classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
- classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
- classes[chroma.LineHighlight] = "display: block; width: 100%;" + classes[chroma.LineHighlight]
- classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block;" + classes[chroma.LineTable]
- classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
- return classes
- }
-
- // StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
- func StyleEntryToCSS(e chroma.StyleEntry) string {
- 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 == chroma.Yes {
- styles = append(styles, "font-weight: bold")
- }
- if e.Italic == chroma.Yes {
- styles = append(styles, "font-style: italic")
- }
- if e.Underline == chroma.Yes {
- styles = append(styles, "text-decoration: underline")
- }
- return strings.Join(styles, "; ")
- }
-
- // Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
- func compressStyle(s string) string {
- parts := strings.Split(s, ";")
- out := []string{}
- for _, p := range parts {
- p = strings.Join(strings.Fields(p), " ")
- p = strings.Replace(p, ": ", ":", 1)
- 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, ";")
- }
|