package svg
import (
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/alecthomas/chroma/v2"
)
type Option func (f *Formatter )
func FontFamily (fontFamily string ) Option { return func (f *Formatter ) { f .fontFamily = fontFamily } }
func EmbedFontFile (fontFamily string , fileName string ) (option Option , err error ) {
var format FontFormat
switch path .Ext (fileName ) {
case ".woff" :
format = WOFF
case ".woff2" :
format = WOFF2
case ".ttf" :
format = TRUETYPE
default :
return nil , errors .New ("unexpected font file suffix" )
}
var content []byte
if content , err = os .ReadFile (fileName ); err == nil {
option = EmbedFont (fontFamily , base64 .StdEncoding .EncodeToString (content ), format )
}
return
}
func EmbedFont (fontFamily string , font string , format FontFormat ) Option {
return func (f *Formatter ) { f .fontFamily = fontFamily ; f .embeddedFont = font ; f .fontFormat = format }
}
func New (options ...Option ) *Formatter {
f := &Formatter {fontFamily : "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace" }
for _ , option := range options {
option (f )
}
return f
}
type Formatter struct {
fontFamily string
embeddedFont string
fontFormat FontFormat
}
func (f *Formatter ) Format (w io .Writer , style *chroma .Style , iterator chroma .Iterator ) (err error ) {
f .writeSVG (w , style , iterator .Tokens ())
return err
}
var svgEscaper = strings .NewReplacer (
`&` , "&" ,
`<` , "<" ,
`>` , ">" ,
`"` , """ ,
` ` , " " ,
` ` , "    " ,
)
func escapeString(s string ) string {
return svgEscaper .Replace (s )
}
func (f *Formatter ) writeSVG (w io .Writer , style *chroma .Style , tokens []chroma .Token ) {
svgStyles := f .styleToSVG (style )
lines := chroma .SplitTokensIntoLines (tokens )
fmt .Fprint (w , "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" )
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" )
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 )))
if f .embeddedFont != "" {
f .writeFontStyle (w )
}
fmt .Fprintf (w , "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n" , style .Get (chroma .Background ).Background .String ())
fmt .Fprintf (w , "<g font-family=\"%s\" font-size=\"14px\" fill=\"%s\">\n" , f .fontFamily , style .Get (chroma .Text ).Colour .String ())
f .writeTokenBackgrounds (w , lines , style )
for index , tokens := range lines {
fmt .Fprintf (w , "<text x=\"0\" y=\"%fem\" xml:space=\"preserve\">" , 1.2 *float64 (index +1 ))
for _ , token := range tokens {
text := escapeString (token .String ())
attr := f .styleAttr (svgStyles , token .Type )
if attr != "" {
text = fmt .Sprintf ("<tspan %s>%s</tspan>" , attr , text )
}
fmt .Fprint (w , text )
}
fmt .Fprint (w , "</text>" )
}
fmt .Fprint (w , "\n</g>\n" )
fmt .Fprint (w , "</svg>\n" )
}
func maxLineWidth(lines [][]chroma .Token ) int {
maxWidth := 0
for _ , tokens := range lines {
length := 0
for _ , token := range tokens {
length += len (strings .ReplaceAll (token .String (), ` ` , " " ))
}
if length > maxWidth {
maxWidth = length
}
}
return maxWidth
}
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 .ReplaceAll (token .String (), ` ` , " " ))
tokenBackground := style .Get (token .Type ).Background
if tokenBackground .IsSet () && tokenBackground != style .Get (chroma .Background ).Background {
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 ())
}
lineLength += length
}
}
}
type FontFormat int
const (
WOFF FontFormat = iota
WOFF2
TRUETYPE
)
var fontFormats = [...]string {
"woff" ,
"woff2" ,
"truetype" ,
}
func (f *Formatter ) writeFontStyle (w io .Writer ) {
fmt .Fprintf (w , `<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>` , f .fontFamily , fontFormats [f .fontFormat ], f .embeddedFont , fontFormats [f .fontFormat ])
}
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 )
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
}
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 , " " )
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .