package svg import ( "fmt" "io" "strings" "github.com/alecthomas/chroma" ) // Option sets an option of the SVG formatter. type Option func(f *Formatter) // New SVG formatter. func New(options ...Option) *Formatter { f := &Formatter{} for _, option := range options { option(f) } return f } // Formatter that generates SVG. type Formatter struct { } func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) { defer func() { if perr := recover(); perr != nil { err = perr.(error) } }() f.writeSVG(w, style, iterator.Tokens()) return err } var svgEscaper = strings.NewReplacer( `&`, "&", `<`, "<", `>`, ">", `"`, """, ` `, " ", ` `, "    ", ) // EscapeString escapes special characters. func escapeString(s string) string { return svgEscaper.Replace(s) } func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo svgStyles := f.styleToSVG(style) lines := chroma.SplitTokensIntoLines(tokens) fmt.Fprint(w, "\n") fmt.Fprint(w, "\n") fmt.Fprintf(w, "\n", len(lines)*22) fmt.Fprintf(w, "\n", style.Get(chroma.Background).Background.String()) fmt.Fprintf(w, "\n", style.Get(chroma.Text).Colour.String()) f.writeTokenBackgrounds(w, lines, style) for index, tokens := range lines { fmt.Fprintf(w, "", (14+7)*(index+1)) for _, token := range tokens { text := escapeString(token.String()) attr := f.styleAttr(svgStyles, token.Type) if attr != "" { text = fmt.Sprintf("%s", attr, text) } fmt.Fprint(w, text) } fmt.Fprint(w, "") } fmt.Fprint(w, "\n\n") fmt.Fprint(w, "\n") } // There is no background attribute for text in SVG so simply calculate the position and text // of tokens with a background color that differs from the default and add a rectangle for each before // adding the token. func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) { for index, tokens := range lines { lineLength := 0 for _, token := range tokens { length := len(strings.Replace(token.String(), ` `, " ", -1)) tokenBackground := style.Get(token.Type).Background if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background { fmt.Fprintf(w, "\n", escapeString(token.String()), lineLength*8, (14+7)*index+7, length*8, 14, style.Get(token.Type).Background.String()) } lineLength += length } } } 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 "" } } } return styles[tt] } func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string { converted := 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 entry.IsZero() { continue } converted[t] = StyleEntryToSVG(entry) } return converted } // StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes. func StyleEntryToSVG(e chroma.StyleEntry) string { var styles []string if e.Colour.IsSet() { styles = append(styles, "fill=\""+e.Colour.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, " ") }