...

Source file src/github.com/alecthomas/chroma/formatters/html/html.go

Documentation: github.com/alecthomas/chroma/formatters/html

     1  package html
     2  
     3  import (
     4  	"fmt"
     5  	"html"
     6  	"io"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/alecthomas/chroma"
    11  )
    12  
    13  // Option sets an option of the HTML formatter.
    14  type Option func(f *Formatter)
    15  
    16  // Standalone configures the HTML formatter for generating a standalone HTML document.
    17  func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
    18  
    19  // ClassPrefix sets the CSS class prefix.
    20  func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
    21  
    22  // WithClasses emits HTML using CSS classes, rather than inline styles.
    23  func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
    24  
    25  // WithAllClasses disables an optimisation that omits redundant CSS classes.
    26  func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
    27  
    28  // TabWidth sets the number of characters for a tab. Defaults to 8.
    29  func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
    30  
    31  // PreventSurroundingPre prevents the surrounding pre tags around the generated code.
    32  func PreventSurroundingPre(b bool) Option {
    33  	return func(f *Formatter) {
    34  		if b {
    35  			f.preWrapper = nopPreWrapper
    36  		} else {
    37  			f.preWrapper = defaultPreWrapper
    38  		}
    39  	}
    40  }
    41  
    42  // WithPreWrapper allows control of the surrounding pre tags.
    43  func WithPreWrapper(wrapper PreWrapper) Option {
    44  	return func(f *Formatter) {
    45  		f.preWrapper = wrapper
    46  	}
    47  }
    48  
    49  // WrapLongLines wraps long lines.
    50  func WrapLongLines(b bool) Option {
    51  	return func(f *Formatter) {
    52  		f.wrapLongLines = b
    53  	}
    54  }
    55  
    56  // WithLineNumbers formats output with line numbers.
    57  func WithLineNumbers(b bool) Option {
    58  	return func(f *Formatter) {
    59  		f.lineNumbers = b
    60  	}
    61  }
    62  
    63  // LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers
    64  // and code in table td's, which make them copy-and-paste friendly.
    65  func LineNumbersInTable(b bool) Option {
    66  	return func(f *Formatter) {
    67  		f.lineNumbersInTable = b
    68  	}
    69  }
    70  
    71  // LinkableLineNumbers decorates the line numbers HTML elements with an "id"
    72  // attribute so they can be linked.
    73  func LinkableLineNumbers(b bool, prefix string) Option {
    74  	return func(f *Formatter) {
    75  		f.linkableLineNumbers = b
    76  		f.lineNumbersIDPrefix = prefix
    77  	}
    78  }
    79  
    80  // HighlightLines higlights the given line ranges with the Highlight style.
    81  //
    82  // A range is the beginning and ending of a range as 1-based line numbers, inclusive.
    83  func HighlightLines(ranges [][2]int) Option {
    84  	return func(f *Formatter) {
    85  		f.highlightRanges = ranges
    86  		sort.Sort(f.highlightRanges)
    87  	}
    88  }
    89  
    90  // BaseLineNumber sets the initial number to start line numbering at. Defaults to 1.
    91  func BaseLineNumber(n int) Option {
    92  	return func(f *Formatter) {
    93  		f.baseLineNumber = n
    94  	}
    95  }
    96  
    97  // New HTML formatter.
    98  func New(options ...Option) *Formatter {
    99  	f := &Formatter{
   100  		baseLineNumber: 1,
   101  		preWrapper:     defaultPreWrapper,
   102  	}
   103  	for _, option := range options {
   104  		option(f)
   105  	}
   106  	return f
   107  }
   108  
   109  // PreWrapper defines the operations supported in WithPreWrapper.
   110  type PreWrapper interface {
   111  	// Start is called to write a start <pre> element.
   112  	// The code flag tells whether this block surrounds
   113  	// highlighted code. This will be false when surrounding
   114  	// line numbers.
   115  	Start(code bool, styleAttr string) string
   116  
   117  	// End is called to write the end </pre> element.
   118  	End(code bool) string
   119  }
   120  
   121  type preWrapper struct {
   122  	start func(code bool, styleAttr string) string
   123  	end   func(code bool) string
   124  }
   125  
   126  func (p preWrapper) Start(code bool, styleAttr string) string {
   127  	return p.start(code, styleAttr)
   128  }
   129  
   130  func (p preWrapper) End(code bool) string {
   131  	return p.end(code)
   132  }
   133  
   134  var (
   135  	nopPreWrapper = preWrapper{
   136  		start: func(code bool, styleAttr string) string { return "" },
   137  		end:   func(code bool) string { return "" },
   138  	}
   139  	defaultPreWrapper = preWrapper{
   140  		start: func(code bool, styleAttr string) string {
   141  			if code {
   142  				return fmt.Sprintf(`<pre tabindex="0"%s><code>`, styleAttr)
   143  			}
   144  
   145  			return fmt.Sprintf(`<pre tabindex="0"%s>`, styleAttr)
   146  		},
   147  		end: func(code bool) string {
   148  			if code {
   149  				return `</code></pre>`
   150  			}
   151  
   152  			return `</pre>`
   153  		},
   154  	}
   155  )
   156  
   157  // Formatter that generates HTML.
   158  type Formatter struct {
   159  	standalone          bool
   160  	prefix              string
   161  	Classes             bool // Exported field to detect when classes are being used
   162  	allClasses          bool
   163  	preWrapper          PreWrapper
   164  	tabWidth            int
   165  	wrapLongLines       bool
   166  	lineNumbers         bool
   167  	lineNumbersInTable  bool
   168  	linkableLineNumbers bool
   169  	lineNumbersIDPrefix string
   170  	highlightRanges     highlightRanges
   171  	baseLineNumber      int
   172  }
   173  
   174  type highlightRanges [][2]int
   175  
   176  func (h highlightRanges) Len() int           { return len(h) }
   177  func (h highlightRanges) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
   178  func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
   179  
   180  func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
   181  	return f.writeHTML(w, style, iterator.Tokens())
   182  }
   183  
   184  // We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked).
   185  //
   186  // OTOH we need to be super careful about correct escaping...
   187  func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
   188  	css := f.styleToCSS(style)
   189  	if !f.Classes {
   190  		for t, style := range css {
   191  			css[t] = compressStyle(style)
   192  		}
   193  	}
   194  	if f.standalone {
   195  		fmt.Fprint(w, "<html>\n")
   196  		if f.Classes {
   197  			fmt.Fprint(w, "<style type=\"text/css\">\n")
   198  			err = f.WriteCSS(w, style)
   199  			if err != nil {
   200  				return err
   201  			}
   202  			fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
   203  			fmt.Fprint(w, "</style>")
   204  		}
   205  		fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
   206  	}
   207  
   208  	wrapInTable := f.lineNumbers && f.lineNumbersInTable
   209  
   210  	lines := chroma.SplitTokensIntoLines(tokens)
   211  	lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
   212  	highlightIndex := 0
   213  
   214  	if wrapInTable {
   215  		// List line numbers in its own <td>
   216  		fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
   217  		fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
   218  		fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
   219  		fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
   220  		for index := range lines {
   221  			line := f.baseLineNumber + index
   222  			highlight, next := f.shouldHighlight(highlightIndex, line)
   223  			if next {
   224  				highlightIndex++
   225  			}
   226  			if highlight {
   227  				fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
   228  			}
   229  
   230  			fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
   231  
   232  			if highlight {
   233  				fmt.Fprintf(w, "</span>")
   234  			}
   235  		}
   236  		fmt.Fprint(w, f.preWrapper.End(false))
   237  		fmt.Fprint(w, "</td>\n")
   238  		fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
   239  	}
   240  
   241  	fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
   242  
   243  	highlightIndex = 0
   244  	for index, tokens := range lines {
   245  		// 1-based line number.
   246  		line := f.baseLineNumber + index
   247  		highlight, next := f.shouldHighlight(highlightIndex, line)
   248  		if next {
   249  			highlightIndex++
   250  		}
   251  
   252  		// Start of Line
   253  		fmt.Fprint(w, `<span`)
   254  		if highlight {
   255  			// Line + LineHighlight
   256  			if f.Classes {
   257  				fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
   258  			} else {
   259  				fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
   260  			}
   261  			fmt.Fprint(w, `>`)
   262  		} else {
   263  			fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
   264  		}
   265  
   266  		// Line number
   267  		if f.lineNumbers && !wrapInTable {
   268  			fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(lineDigits, line))
   269  		}
   270  
   271  		fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
   272  
   273  		for _, token := range tokens {
   274  			html := html.EscapeString(token.String())
   275  			attr := f.styleAttr(css, token.Type)
   276  			if attr != "" {
   277  				html = fmt.Sprintf("<span%s>%s</span>", attr, html)
   278  			}
   279  			fmt.Fprint(w, html)
   280  		}
   281  
   282  		fmt.Fprint(w, `</span>`) // End of CodeLine
   283  
   284  		fmt.Fprint(w, `</span>`) // End of Line
   285  	}
   286  
   287  	fmt.Fprintf(w, f.preWrapper.End(true))
   288  
   289  	if wrapInTable {
   290  		fmt.Fprint(w, "</td></tr></table>\n")
   291  		fmt.Fprint(w, "</div>\n")
   292  	}
   293  
   294  	if f.standalone {
   295  		fmt.Fprint(w, "\n</body>\n")
   296  		fmt.Fprint(w, "</html>\n")
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  func (f *Formatter) lineIDAttribute(line int) string {
   303  	if !f.linkableLineNumbers {
   304  		return ""
   305  	}
   306  	return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
   307  }
   308  
   309  func (f *Formatter) lineTitleWithLinkIfNeeded(lineDigits, line int) string {
   310  	title := fmt.Sprintf("%*d", lineDigits, line)
   311  	if !f.linkableLineNumbers {
   312  		return title
   313  	}
   314  	return fmt.Sprintf("<a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#%s\">%s</a>", f.lineID(line), title)
   315  }
   316  
   317  func (f *Formatter) lineID(line int) string {
   318  	return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
   319  }
   320  
   321  func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
   322  	next := false
   323  	for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
   324  		highlightIndex++
   325  		next = true
   326  	}
   327  	if highlightIndex < len(f.highlightRanges) {
   328  		hrange := f.highlightRanges[highlightIndex]
   329  		if line >= hrange[0] && line <= hrange[1] {
   330  			return true, next
   331  		}
   332  	}
   333  	return false, next
   334  }
   335  
   336  func (f *Formatter) class(t chroma.TokenType) string {
   337  	for t != 0 {
   338  		if cls, ok := chroma.StandardTypes[t]; ok {
   339  			if cls != "" {
   340  				return f.prefix + cls
   341  			}
   342  			return ""
   343  		}
   344  		t = t.Parent()
   345  	}
   346  	if cls := chroma.StandardTypes[t]; cls != "" {
   347  		return f.prefix + cls
   348  	}
   349  	return ""
   350  }
   351  
   352  func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
   353  	if f.Classes {
   354  		cls := f.class(tt)
   355  		if cls == "" {
   356  			return ""
   357  		}
   358  		return fmt.Sprintf(` class="%s"`, cls)
   359  	}
   360  	if _, ok := styles[tt]; !ok {
   361  		tt = tt.SubCategory()
   362  		if _, ok := styles[tt]; !ok {
   363  			tt = tt.Category()
   364  			if _, ok := styles[tt]; !ok {
   365  				return ""
   366  			}
   367  		}
   368  	}
   369  	css := []string{styles[tt]}
   370  	css = append(css, extraCSS...)
   371  	return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
   372  }
   373  
   374  func (f *Formatter) tabWidthStyle() string {
   375  	if f.tabWidth != 0 && f.tabWidth != 8 {
   376  		return fmt.Sprintf("; -moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d", f.tabWidth)
   377  	}
   378  	return ""
   379  }
   380  
   381  // WriteCSS writes CSS style definitions (without any surrounding HTML).
   382  func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
   383  	css := f.styleToCSS(style)
   384  	// Special-case background as it is mapped to the outer ".chroma" class.
   385  	if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
   386  		return err
   387  	}
   388  	// Special-case PreWrapper as it is the ".chroma" class.
   389  	if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
   390  		return err
   391  	}
   392  	// Special-case code column of table to expand width.
   393  	if f.lineNumbers && f.lineNumbersInTable {
   394  		if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
   395  			chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
   396  			return err
   397  		}
   398  	}
   399  	// Special-case line number highlighting when targeted.
   400  	if f.lineNumbers || f.lineNumbersInTable {
   401  		targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
   402  		for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
   403  			fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
   404  		}
   405  	}
   406  	tts := []int{}
   407  	for tt := range css {
   408  		tts = append(tts, int(tt))
   409  	}
   410  	sort.Ints(tts)
   411  	for _, ti := range tts {
   412  		tt := chroma.TokenType(ti)
   413  		switch tt {
   414  		case chroma.Background, chroma.PreWrapper:
   415  			continue
   416  		}
   417  		class := f.class(tt)
   418  		if class == "" {
   419  			continue
   420  		}
   421  		styles := css[tt]
   422  		if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
   423  			return err
   424  		}
   425  	}
   426  	return nil
   427  }
   428  
   429  func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
   430  	classes := map[chroma.TokenType]string{}
   431  	bg := style.Get(chroma.Background)
   432  	// Convert the style.
   433  	for t := range chroma.StandardTypes {
   434  		entry := style.Get(t)
   435  		if t != chroma.Background {
   436  			entry = entry.Sub(bg)
   437  		}
   438  		if !f.allClasses && entry.IsZero() {
   439  			continue
   440  		}
   441  		classes[t] = StyleEntryToCSS(entry)
   442  	}
   443  	classes[chroma.Background] += f.tabWidthStyle()
   444  	classes[chroma.PreWrapper] += classes[chroma.Background] + `;`
   445  	// Make PreWrapper a grid to show highlight style with full width.
   446  	if len(f.highlightRanges) > 0 {
   447  		classes[chroma.PreWrapper] += `display: grid;`
   448  	}
   449  	// Make PreWrapper wrap long lines.
   450  	if f.wrapLongLines {
   451  		classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
   452  	}
   453  	lineNumbersStyle := `white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
   454  	// All rules begin with default rules followed by user provided rules
   455  	classes[chroma.Line] = `display: flex;` + classes[chroma.Line]
   456  	classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
   457  	classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
   458  	classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
   459  	classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
   460  	return classes
   461  }
   462  
   463  // StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
   464  func StyleEntryToCSS(e chroma.StyleEntry) string {
   465  	styles := []string{}
   466  	if e.Colour.IsSet() {
   467  		styles = append(styles, "color: "+e.Colour.String())
   468  	}
   469  	if e.Background.IsSet() {
   470  		styles = append(styles, "background-color: "+e.Background.String())
   471  	}
   472  	if e.Bold == chroma.Yes {
   473  		styles = append(styles, "font-weight: bold")
   474  	}
   475  	if e.Italic == chroma.Yes {
   476  		styles = append(styles, "font-style: italic")
   477  	}
   478  	if e.Underline == chroma.Yes {
   479  		styles = append(styles, "text-decoration: underline")
   480  	}
   481  	return strings.Join(styles, "; ")
   482  }
   483  
   484  // Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
   485  func compressStyle(s string) string {
   486  	parts := strings.Split(s, ";")
   487  	out := []string{}
   488  	for _, p := range parts {
   489  		p = strings.Join(strings.Fields(p), " ")
   490  		p = strings.Replace(p, ": ", ":", 1)
   491  		if strings.Contains(p, "#") {
   492  			c := p[len(p)-6:]
   493  			if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
   494  				p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
   495  			}
   496  		}
   497  		out = append(out, p)
   498  	}
   499  	return strings.Join(out, ";")
   500  }
   501  

View as plain text