...

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

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

     1  // Package svg contains an SVG formatter.
     2  package svg
     3  
     4  import (
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/alecthomas/chroma"
    14  )
    15  
    16  // Option sets an option of the SVG formatter.
    17  type Option func(f *Formatter)
    18  
    19  // FontFamily sets the font-family.
    20  func FontFamily(fontFamily string) Option { return func(f *Formatter) { f.fontFamily = fontFamily } }
    21  
    22  // EmbedFontFile embeds given font file
    23  func EmbedFontFile(fontFamily string, fileName string) (option Option, err error) {
    24  	var format FontFormat
    25  	switch path.Ext(fileName) {
    26  	case ".woff":
    27  		format = WOFF
    28  	case ".woff2":
    29  		format = WOFF2
    30  	case ".ttf":
    31  		format = TRUETYPE
    32  	default:
    33  		return nil, errors.New("unexpected font file suffix")
    34  	}
    35  
    36  	var content []byte
    37  	if content, err = ioutil.ReadFile(fileName); err == nil {
    38  		option = EmbedFont(fontFamily, base64.StdEncoding.EncodeToString(content), format)
    39  	}
    40  	return
    41  }
    42  
    43  // EmbedFont embeds given base64 encoded font
    44  func EmbedFont(fontFamily string, font string, format FontFormat) Option {
    45  	return func(f *Formatter) { f.fontFamily = fontFamily; f.embeddedFont = font; f.fontFormat = format }
    46  }
    47  
    48  // New SVG formatter.
    49  func New(options ...Option) *Formatter {
    50  	f := &Formatter{fontFamily: "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace"}
    51  	for _, option := range options {
    52  		option(f)
    53  	}
    54  	return f
    55  }
    56  
    57  // Formatter that generates SVG.
    58  type Formatter struct {
    59  	fontFamily   string
    60  	embeddedFont string
    61  	fontFormat   FontFormat
    62  }
    63  
    64  func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
    65  	f.writeSVG(w, style, iterator.Tokens())
    66  	return err
    67  }
    68  
    69  var svgEscaper = strings.NewReplacer(
    70  	`&`, "&",
    71  	`<`, "&lt;",
    72  	`>`, "&gt;",
    73  	`"`, "&quot;",
    74  	` `, "&#160;",
    75  	`	`, "&#160;&#160;&#160;&#160;",
    76  )
    77  
    78  // EscapeString escapes special characters.
    79  func escapeString(s string) string {
    80  	return svgEscaper.Replace(s)
    81  }
    82  
    83  func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo
    84  	svgStyles := f.styleToSVG(style)
    85  	lines := chroma.SplitTokensIntoLines(tokens)
    86  
    87  	fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
    88  	fmt.Fprint(w, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
    89  	fmt.Fprintf(w, "<svg width=\"%dpx\" height=\"%dpx\" xmlns=\"http://www.w3.org/2000/svg\">\n", 8*maxLineWidth(lines), 10+int(16.8*float64(len(lines)+1)))
    90  
    91  	if f.embeddedFont != "" {
    92  		f.writeFontStyle(w)
    93  	}
    94  
    95  	fmt.Fprintf(w, "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n", style.Get(chroma.Background).Background.String())
    96  	fmt.Fprintf(w, "<g font-family=\"%s\" font-size=\"14px\" fill=\"%s\">\n", f.fontFamily, style.Get(chroma.Text).Colour.String())
    97  
    98  	f.writeTokenBackgrounds(w, lines, style)
    99  
   100  	for index, tokens := range lines {
   101  		fmt.Fprintf(w, "<text x=\"0\" y=\"%fem\" xml:space=\"preserve\">", 1.2*float64(index+1))
   102  
   103  		for _, token := range tokens {
   104  			text := escapeString(token.String())
   105  			attr := f.styleAttr(svgStyles, token.Type)
   106  			if attr != "" {
   107  				text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
   108  			}
   109  			fmt.Fprint(w, text)
   110  		}
   111  		fmt.Fprint(w, "</text>")
   112  	}
   113  
   114  	fmt.Fprint(w, "\n</g>\n")
   115  	fmt.Fprint(w, "</svg>\n")
   116  }
   117  
   118  func maxLineWidth(lines [][]chroma.Token) int {
   119  	maxWidth := 0
   120  	for _, tokens := range lines {
   121  		length := 0
   122  		for _, token := range tokens {
   123  			length += len(strings.ReplaceAll(token.String(), `	`, "    "))
   124  		}
   125  		if length > maxWidth {
   126  			maxWidth = length
   127  		}
   128  	}
   129  	return maxWidth
   130  }
   131  
   132  // There is no background attribute for text in SVG so simply calculate the position and text
   133  // of tokens with a background color that differs from the default and add a rectangle for each before
   134  // adding the token.
   135  func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) {
   136  	for index, tokens := range lines {
   137  		lineLength := 0
   138  		for _, token := range tokens {
   139  			length := len(strings.ReplaceAll(token.String(), `	`, "    "))
   140  			tokenBackground := style.Get(token.Type).Background
   141  			if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
   142  				fmt.Fprintf(w, "<rect id=\"%s\" x=\"%dch\" y=\"%fem\" width=\"%dch\" height=\"1.2em\" fill=\"%s\" />\n", escapeString(token.String()), lineLength, 1.2*float64(index)+0.25, length, style.Get(token.Type).Background.String())
   143  			}
   144  			lineLength += length
   145  		}
   146  	}
   147  }
   148  
   149  type FontFormat int
   150  
   151  // https://transfonter.org/formats
   152  const (
   153  	WOFF FontFormat = iota
   154  	WOFF2
   155  	TRUETYPE
   156  )
   157  
   158  var fontFormats = [...]string{
   159  	"woff",
   160  	"woff2",
   161  	"truetype",
   162  }
   163  
   164  func (f *Formatter) writeFontStyle(w io.Writer) {
   165  	fmt.Fprintf(w, `<style>
   166  @font-face {
   167  	font-family: '%s';
   168  	src: url(data:application/x-font-%s;charset=utf-8;base64,%s) format('%s');'
   169  	font-weight: normal;
   170  	font-style: normal;
   171  }
   172  </style>`, f.fontFamily, fontFormats[f.fontFormat], f.embeddedFont, fontFormats[f.fontFormat])
   173  }
   174  
   175  func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
   176  	if _, ok := styles[tt]; !ok {
   177  		tt = tt.SubCategory()
   178  		if _, ok := styles[tt]; !ok {
   179  			tt = tt.Category()
   180  			if _, ok := styles[tt]; !ok {
   181  				return ""
   182  			}
   183  		}
   184  	}
   185  	return styles[tt]
   186  }
   187  
   188  func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string {
   189  	converted := map[chroma.TokenType]string{}
   190  	bg := style.Get(chroma.Background)
   191  	// Convert the style.
   192  	for t := range chroma.StandardTypes {
   193  		entry := style.Get(t)
   194  		if t != chroma.Background {
   195  			entry = entry.Sub(bg)
   196  		}
   197  		if entry.IsZero() {
   198  			continue
   199  		}
   200  		converted[t] = StyleEntryToSVG(entry)
   201  	}
   202  	return converted
   203  }
   204  
   205  // StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes.
   206  func StyleEntryToSVG(e chroma.StyleEntry) string {
   207  	var styles []string
   208  
   209  	if e.Colour.IsSet() {
   210  		styles = append(styles, "fill=\""+e.Colour.String()+"\"")
   211  	}
   212  	if e.Bold == chroma.Yes {
   213  		styles = append(styles, "font-weight=\"bold\"")
   214  	}
   215  	if e.Italic == chroma.Yes {
   216  		styles = append(styles, "font-style=\"italic\"")
   217  	}
   218  	if e.Underline == chroma.Yes {
   219  		styles = append(styles, "text-decoration=\"underline\"")
   220  	}
   221  	return strings.Join(styles, " ")
   222  }
   223  

View as plain text