diff --git a/cmd/chroma/main.go b/cmd/chroma/main.go
index ff79929..1240e45 100644
--- a/cmd/chroma/main.go
+++ b/cmd/chroma/main.go
@@ -64,6 +64,8 @@ command, for Go.
HTMLBaseLine int `help:"Base line number." default:"1"`
HTMLPreventSurroundingPre bool `help:"Prevent the surrounding pre tag."`
+ SVG bool `help:"Output SVG representation of tokens."`
+
Files []string `arg:"" optional:"" help:"Files to highlight." type:"existingfile"`
}
)
@@ -123,6 +125,10 @@ func main() {
cli.Formatter = "html"
}
+ if cli.SVG {
+ cli.Formatter = "svg"
+ }
+
// Retrieve user-specified style, clone it, and add some overrides.
builder := styles.Get(cli.Style).Builder()
if cli.HTMLHighlightStyle != "" {
diff --git a/formatters/api.go b/formatters/api.go
index 03e4bab..8d9fcfd 100644
--- a/formatters/api.go
+++ b/formatters/api.go
@@ -6,6 +6,7 @@ import (
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/formatters/svg"
)
var (
@@ -20,6 +21,7 @@ var (
}))
// Default HTML formatter outputs self-contained HTML.
htmlFull = Register("html", html.New(html.Standalone(), html.WithClasses())) // nolint
+ Svg = Register("svg", svg.New())
)
// Fallback formatter.
diff --git a/formatters/svg/svg.go b/formatters/svg/svg.go
new file mode 100644
index 0000000..d340e97
--- /dev/null
+++ b/formatters/svg/svg.go
@@ -0,0 +1,145 @@
+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")
+}
+
+// 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, " ")
+}