// Package svg contains an SVG formatter.
package svg import ( ) // Option sets an option of the SVG formatter. type Option func(f *Formatter) // FontFamily sets the font-family. func ( string) Option { return func( *Formatter) { .fontFamily = } } // EmbedFontFile embeds given font file func ( string, string) ( Option, error) { var FontFormat switch path.Ext() { case ".woff": = WOFF case ".woff2": = WOFF2 case ".ttf": = TRUETYPE default: return nil, errors.New("unexpected font file suffix") } var []byte if , = os.ReadFile(); == nil { = EmbedFont(, base64.StdEncoding.EncodeToString(), ) } return } // EmbedFont embeds given base64 encoded font func ( string, string, FontFormat) Option { return func( *Formatter) { .fontFamily = ; .embeddedFont = ; .fontFormat = } } // New SVG formatter. func ( ...Option) *Formatter { := &Formatter{fontFamily: "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace"} for , := range { () } return } // Formatter that generates SVG. type Formatter struct { fontFamily string embeddedFont string fontFormat FontFormat } func ( *Formatter) ( io.Writer, *chroma.Style, chroma.Iterator) ( error) { .writeSVG(, , .Tokens()) return } var svgEscaper = strings.NewReplacer( `&`, "&amp;", `<`, "&lt;", `>`, "&gt;", `"`, "&quot;", ` `, "&#160;", ` `, "&#160;&#160;&#160;&#160;", ) // EscapeString escapes special characters. func escapeString( string) string { return svgEscaper.Replace() } func ( *Formatter) ( io.Writer, *chroma.Style, []chroma.Token) { // nolint: gocyclo := .styleToSVG() := chroma.SplitTokensIntoLines() fmt.Fprint(, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") fmt.Fprint(, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n") fmt.Fprintf(, "<svg width=\"%dpx\" height=\"%dpx\" xmlns=\"http://www.w3.org/2000/svg\">\n", 8*maxLineWidth(), 10+int(16.8*float64(len()+1))) if .embeddedFont != "" { .writeFontStyle() } fmt.Fprintf(, "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n", .Get(chroma.Background).Background.String()) fmt.Fprintf(, "<g font-family=\"%s\" font-size=\"14px\" fill=\"%s\">\n", .fontFamily, .Get(chroma.Text).Colour.String()) .writeTokenBackgrounds(, , ) for , := range { fmt.Fprintf(, "<text x=\"0\" y=\"%fem\" xml:space=\"preserve\">", 1.2*float64(+1)) for , := range { := escapeString(.String()) := .styleAttr(, .Type) if != "" { = fmt.Sprintf("<tspan %s>%s</tspan>", , ) } fmt.Fprint(, ) } fmt.Fprint(, "</text>") } fmt.Fprint(, "\n</g>\n") fmt.Fprint(, "</svg>\n") } func maxLineWidth( [][]chroma.Token) int { := 0 for , := range { := 0 for , := range { += len(strings.ReplaceAll(.String(), ` `, " ")) } if > { = } } return } // 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 ( *Formatter) ( io.Writer, [][]chroma.Token, *chroma.Style) { for , := range { := 0 for , := range { := len(strings.ReplaceAll(.String(), ` `, " ")) := .Get(.Type).Background if .IsSet() && != .Get(chroma.Background).Background { fmt.Fprintf(, "<rect id=\"%s\" x=\"%dch\" y=\"%fem\" width=\"%dch\" height=\"1.2em\" fill=\"%s\" />\n", escapeString(.String()), , 1.2*float64()+0.25, , .Get(.Type).Background.String()) } += } } } type FontFormat int // https://transfonter.org/formats const ( WOFF FontFormat = iota WOFF2 TRUETYPE ) var fontFormats = [...]string{ "woff", "woff2", "truetype", } func ( *Formatter) ( io.Writer) { fmt.Fprintf(, `<style> @font-face { font-family: '%s'; src: url(data:application/x-font-%s;charset=utf-8;base64,%s) format('%s');' font-weight: normal; font-style: normal; } </style>`, .fontFamily, fontFormats[.fontFormat], .embeddedFont, fontFormats[.fontFormat]) } func ( *Formatter) ( map[chroma.TokenType]string, chroma.TokenType) string { if , := []; ! { = .SubCategory() if , := []; ! { = .Category() if , := []; ! { return "" } } } return [] } func ( *Formatter) ( *chroma.Style) map[chroma.TokenType]string { := map[chroma.TokenType]string{} := .Get(chroma.Background) // Convert the style. for := range chroma.StandardTypes { := .Get() if != chroma.Background { = .Sub() } if .IsZero() { continue } [] = StyleEntryToSVG() } return } // StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes. func ( chroma.StyleEntry) string { var []string if .Colour.IsSet() { = append(, "fill=\""+.Colour.String()+"\"") } if .Bold == chroma.Yes { = append(, "font-weight=\"bold\"") } if .Italic == chroma.Yes { = append(, "font-style=\"italic\"") } if .Underline == chroma.Yes { = append(, "text-decoration=\"underline\"") } return strings.Join(, " ") }